diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..449691b7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-exact=true \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index df4865d2..071bf4c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js node_js: +<<<<<<< HEAD - "4.2.6" before_script: @@ -9,3 +10,12 @@ script: - npm run test - npm run test-e2e +======= + - "5.6.0" + +before_install: + - npm install -g bower gulp + +script: + - npm run test +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 diff --git a/README.md b/README.md index 6f326e6d..cff5d7d4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ A seed project for custom Four51 Solutions built on AngularJS *** +<<<<<<< HEAD +======= +## Build Status +### development +[![Build Status](https://travis-ci.org/ordercloud-api/angular-buyer.svg?branch=development)](https://travis-ci.org/ordercloud-api/angular-buyer) +### master +[![Build Status](https://travis-ci.org/ordercloud-api/angular-buyer.svg?branch=master)](https://travis-ci.org/ordercloud-api/angular-buyer) + +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 ## Get started Node.js is required for the following node package manager (npm) tasks. If you don't have node.js installed, you can download it [here](http://nodejs.org/). @@ -144,4 +153,8 @@ task, which runs `build` and then `compile`: ```sh $ gulp -``` \ No newline at end of file +<<<<<<< HEAD +``` +======= +``` +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 diff --git a/bower.json b/bower.json index b4fd13fd..e348e816 100644 --- a/bower.json +++ b/bower.json @@ -24,6 +24,7 @@ ], "dependencies": { "bootstrap": "^3.3.6", +<<<<<<< HEAD "angular": "^1.5.7", "angular-animate": "^1.5.7", "angular-sanitize": "^1.5.7", @@ -47,5 +48,34 @@ "resolutions": { "angular": "^1.5.7", "angular-animate": "^1.5.7" +======= + "angular": "^1.6.0", + "angular-resource": "^1.6.0", + "angular-animate": "^1.6.0", + "angular-sanitize": "^1.6.0", + "angular-touch": "^1.6.0", + "angular-messages": "^1.6.0", + "angular-toastr": "^1.7.0", + "angular-ui-tree": "^2.16.0", + "angular-bootstrap": "^1.3.3", + "ordercloud-ng-sdk": "1.0.27", + "angular-auto-validate": "^1.19.5", + "font-awesome": "^4.6.3", + "angular-localforage": "^1.2.5", + "underscore": "^1.8.3", + "angular-ui-router": "^0.3.1", + "angular-tree-control": "^0.2.28", + "angular-sticky": "angular-sticky-plugin#^0.3.0", + "angular-busy2": "^5.2.0" + }, + "devDependencies": { + "angular-mocks": "^1.6.0" + }, + "resolutions": { + "angular": "^1.6.0", + "angular-animate": "^1.6.0", + "angular-resource": "^1.6.0", + "angular-cookies": "^1.6.0" +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 } } diff --git a/gulp.config.js b/gulp.config.js index c07c8ed3..78cb0584 100644 --- a/gulp.config.js +++ b/gulp.config.js @@ -1,6 +1,9 @@ var source = './src/', assets = 'assets/', +<<<<<<< HEAD components = './../Components/', +======= +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 build = './build/', bowerFiles = './bower_components/', npmFiles = './node_modules', @@ -36,6 +39,7 @@ module.exports = { '!' + source + '**/*.spec.js', '!' + source + '**/*.test.js' ], +<<<<<<< HEAD components: { dir: components, scripts: [ @@ -51,6 +55,13 @@ module.exports = { }, appFiles: [ build + '**/app.js', +======= + appFiles: [ + build + '**/app.module.js', + build + '**/app.config.js', + build + '**/app.run.js', + build + '**/app.controller.js', +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 build + '**/*.js', build + '**/*.css', source + '**/*.css' @@ -79,7 +90,11 @@ module.exports = { function getConstants() { var result = {}; +<<<<<<< HEAD var constants = JSON.parse(fs.readFileSync(source + 'app/app.config.json')); +======= + var constants = JSON.parse(fs.readFileSync(source + 'app/app.constants.json')); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 var environment = process.env.environment || constants.environment; switch (environment) { case 'local': diff --git a/gulp/build/app-config.js b/gulp/build/app-config.js index 68160ff7..d9b7ca59 100644 --- a/gulp/build/app-config.js +++ b/gulp/build/app-config.js @@ -4,12 +4,20 @@ var gulp = require('gulp'), ngConstant = require('gulp-ng-constant'); gulp.task('clean:app-config', function() { +<<<<<<< HEAD return del(config.build + '**/app.config.js'); +======= + return del(config.build + '**/app.constants.js'); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }); gulp.task('app-config', ['clean:app-config'], function() { return gulp +<<<<<<< HEAD .src(config.src + '**/app.config.json') +======= + .src(config.src + '**/app.constants.json') +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 .pipe(ngConstant(config.ngConstantSettings)) .pipe(gulp.dest(config.build)); }); diff --git a/gulp/build/inject.js b/gulp/build/inject.js index 888fcf5f..63e77c71 100644 --- a/gulp/build/inject.js +++ b/gulp/build/inject.js @@ -11,10 +11,18 @@ gulp.task('clean:inject', function() { gulp.task('inject', ['clean:inject', 'scripts', 'assets', 'app-config', 'bower-fonts', 'styles'], function() { var target = gulp.src(config.index), bowerFiles = gulp.src(mainBowerFiles({filter: ['**/*.js', '**/*.css']}), {read: false}), +<<<<<<< HEAD appFiles = gulp.src([].concat(config.appFiles, config.components.styles.css), {read: false}); return target .pipe(inject(bowerFiles, {name: 'bower'})) .pipe(inject(appFiles)) +======= + appFiles = gulp.src([].concat(config.appFiles), {read: false}); + + return target + .pipe(inject(bowerFiles, {name: 'bower', ignorePath: config.bowerFiles.replace('.', ''), addPrefix: 'bower_files'})) + .pipe(inject(appFiles, {ignorePath: config.build.replace('.', '')})) +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 .pipe(gulp.dest(config.build)); }); diff --git a/gulp/build/scripts.js b/gulp/build/scripts.js index fd646e9d..fbe9ab23 100644 --- a/gulp/build/scripts.js +++ b/gulp/build/scripts.js @@ -11,15 +11,23 @@ var gulp = require('gulp'), gulp.task('clean:scripts', function() { return del([ config.build + '**/*.js', +<<<<<<< HEAD '!' + config.build + '**/app.config.js' +======= + '!' + config.build + '**/app.constants.js' +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 ]); }); gulp.task('scripts', ['clean:scripts'], function() { return gulp .src([].concat( +<<<<<<< HEAD config.scripts, config.components.scripts +======= + config.scripts +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 )) .pipe(cache(config.jsCache)) .pipe(ngAnnotate()) @@ -31,8 +39,12 @@ gulp.task('scripts', ['clean:scripts'], function() { gulp.task('rebuild-scripts', function() { return gulp .src([].concat( +<<<<<<< HEAD config.scripts, config.components.scripts +======= + config.scripts +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 )) .pipe(cache('jsscripts')) .pipe(ngAnnotate()) diff --git a/gulp/build/serve-build.js b/gulp/build/serve-build.js index 6aa9133e..8bfd567f 100644 --- a/gulp/build/serve-build.js +++ b/gulp/build/serve-build.js @@ -1,21 +1,31 @@ var gulp = require('gulp'), config = require('../../gulp.config'), +<<<<<<< HEAD browserSync = require('browser-sync'), +======= +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 argv = require('yargs') .count('debug') .alias('d', 'debug') .argv, +<<<<<<< HEAD serve = require('../serve'), unit = require('../test/unit'), plato = require('../test/plato'); +======= + serve = require('../serve'); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 gulp.task('serve-build', ['inject'], function() { serve(true /*isDev*/); if (argv.debug) { +<<<<<<< HEAD unit.RunUnitTests(); unit.ServeTests(); plato.GenerateReport(function () { plato.OpenReport(); }); +======= +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 } }); diff --git a/gulp/build/styles.js b/gulp/build/styles.js index 620cfbca..25bf547a 100644 --- a/gulp/build/styles.js +++ b/gulp/build/styles.js @@ -18,8 +18,12 @@ gulp.task('styles', ['clean:styles'], function() { return gulp .src([].concat( mainBowerFiles({filter: '**/*.less'}), +<<<<<<< HEAD config.src + '**/*.less', config.components.styles.less +======= + './src/app/styles/main.less' +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 )) .pipe(sourcemaps.init()) .pipe(lessImport('oc-import.less')) diff --git a/gulp/compile/app-css.js b/gulp/compile/app-css.js index 830c8efd..af8f7bf0 100644 --- a/gulp/compile/app-css.js +++ b/gulp/compile/app-css.js @@ -21,9 +21,13 @@ gulp.task('app-css', ['clean:app-css'], function() { return gulp .src([].concat( mainBowerFiles({filter: ['**/*.css', '**/*.less']}), +<<<<<<< HEAD config.components.styles.less, config.components.styles.css, config.styles +======= + './src/app/styles/main.less' +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 )) .pipe(lessFilter) .pipe(lessImport('app.less')) diff --git a/gulp/compile/app-js.js b/gulp/compile/app-js.js index 6962f758..1928427d 100644 --- a/gulp/compile/app-js.js +++ b/gulp/compile/app-js.js @@ -24,10 +24,15 @@ gulp.task('app-js', ['clean:app-js'], function() { return gulp .src([].concat( config.scripts, +<<<<<<< HEAD config.components.scripts, config.templates, config.components.templates, config.src + '**/app.config.json' +======= + config.templates, + config.src + '**/app.constants.json' +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 )) .pipe(jsonFilter) .pipe(ngConstant(config.ngConstantSettings)) @@ -40,7 +45,14 @@ gulp.task('app-js', ['clean:app-js'], function() { .pipe(fileSort()) .pipe(wrapper(config.wrapper)) .pipe(ngAnnotate()) +<<<<<<< HEAD .pipe(concat('app.js')) +======= + .pipe(concat('app.module.js')) + .pipe(concat('app.config.js')) + .pipe(concat('app.run.js')) + .pipe(concat('app.controller.js')) +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 .pipe(rev()) .pipe(uglify({mangle:false})) //turning off mangle to fix the compile error .pipe(gulp.dest(config.compile + 'js/')) diff --git a/gulp/serve.js b/gulp/serve.js index 3cd7fb3d..c31c88e1 100644 --- a/gulp/serve.js +++ b/gulp/serve.js @@ -30,6 +30,7 @@ module.exports = function (isDev) { if (isDev) { gulp.watch([].concat( +<<<<<<< HEAD config.src + '**/*.html', config.components.templates )) @@ -45,6 +46,19 @@ config.components.styles.css ), ['styles']); gulp.watch(config.src + '**/app.config.json', ['app-config']) +======= + config.src + '**/*.html' + )) + .on('change', browserSync.reload); + gulp.watch([].concat( + config.scripts + ), ['rebuild-scripts']) + .on('change', browserSync.reload); + gulp.watch([].concat( + config.styles + ), ['styles']); + gulp.watch(config.src + '**/app.constants.json', ['app-config']) +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 .on('change', browserSync.reload); } return nodemon ({ diff --git a/gulp/test/e2e.js b/gulp/test/e2e.js index 2ae9e302..205ad21a 100644 --- a/gulp/test/e2e.js +++ b/gulp/test/e2e.js @@ -1,4 +1,5 @@ var gulp = require('gulp'), +<<<<<<< HEAD argv = require('yargs').argv, selenium = require('selenium-standalone'), config = require('../../gulp.config'), @@ -59,5 +60,38 @@ gulp.task('test:e2e', ['http'/*, 'selenium'*/], function() { .once('end', function() { browserSync.exit(); selenium.child.kill(); +======= + browserSync = require('browser-sync').create(), + argv = require('yargs').argv, + config = require('../../gulp.config'), + protractor = require("gulp-protractor").protractor; + +gulp.task('start-localhost', ['inject'], function() { + browserSync.init({ + server: { + baseDir: [ + config.root + config.build.replace('.', ''), + config.root + config.src.replace('.', '') + 'app/' + ], + routes: { + '/bower_files': config.root + config.bowerFiles.replace('.', '') + } + }, + open: false + }); +}); + +gulp.task('test:e2e', ['start-localhost'], function() { + gulp.src([config.src + '**/*.test.js']) + .pipe(protractor({ + configFile: config.root + '/protractor.conf.js', + args: [ + '--suite', argv.suite || 'all' + ] + })) + .on('end', browserSync.exit) + .on('error', function (e) { + throw e; +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }); }); diff --git a/gulp/test/unit.js b/gulp/test/unit.js index bff79e46..d270d494 100644 --- a/gulp/test/unit.js +++ b/gulp/test/unit.js @@ -1,4 +1,5 @@ var gulp = require('gulp'), +<<<<<<< HEAD inject = require('gulp-inject'), browserSync = require('browser-sync').create(), config = require('../../gulp.config'), @@ -63,3 +64,14 @@ module.exports = { ServeTests: serveTests }; +======= + config = require('../../gulp.config'), + Server = require('karma').Server; + +gulp.task('test:unit', ['scripts', 'app-config'], function(done) { + new Server({ + configFile: config.root + '/karma.conf.js', + singleRun: true + }, done).start(); +}); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 diff --git a/heroku.gulpfile.js b/heroku.gulpfile.js new file mode 100644 index 00000000..2a1fbc51 --- /dev/null +++ b/heroku.gulpfile.js @@ -0,0 +1,12 @@ +'use strict'; + +var requireDir = require('require-dir'), + gulp = require('gulp'); + +requireDir('./gulp/build', {recurse: false}); +requireDir('./gulp/compile', {recurse: false}); + +gulp.task('build', ['serve-build']); +gulp.task('compile', ['serve-compile']); + +gulp.task('default', ['inject', 'index']); \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 00000000..1e8be11b --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,92 @@ +// Karma configuration +// Generated on Mon Oct 03 2016 09:44:56 GMT-0500 (Central Daylight Time) + +module.exports = function(config) { + var mainBowerFiles = require('main-bower-files'); + var path = require('path'); + + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: __dirname, + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jasmine'], + + + // list of files / patterns to load in the browser + files: [].concat( + mainBowerFiles({filter: '**/*.js'}), + 'bower_components/angular-mocks/angular-mocks.js', + 'build/**/app.module.js', + 'build/**/app.*.js', + 'build/**/*.js', + 'src/**/*.spec.js', + '../Components/**/*.spec.js', + 'src/app/**/*.html', + '../Components/**/*.html' + ), + + // list of files to exclude + exclude: [ + 'build/**/*.test.js', + '../Components/**/*.test.js', + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + '**/*.html': ['ng-html2js'], + '../Components/**/*.html': ['ng-html2js'] + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['mocha'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity, + + ngHtml2JsPreprocessor: { + //stripPrefix: 'src/app/', + cacheIdFromPath: function(filepath) { + filepath = filepath.replace('src/app', ''); + filepath = filepath.replace((path.join(__dirname, '../Components/').replace(/\\/g,"/")), ''); + return filepath; + }, + moduleName: 'orderCloud' + } + }); +} \ No newline at end of file diff --git a/package.json b/package.json index de2f6623..e178519f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,11 @@ "name": "ordercloud-seed", "version": "1.0.0", "description": "A seed project for custom Four51 Solutions built on AngularJS", +<<<<<<< HEAD "main": "app.js", +======= + "main": "app.module.js", +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 "engines": { "node": "5.6.0", "npm": "3.6.0" @@ -12,11 +16,19 @@ "bower_components" ], "scripts": { +<<<<<<< HEAD "postinstall": "bower install && npm run build-all", "start": "node server.js", "build-all": "gulp inject index", "test": "gulp test:unit", "test-e2e": "gulp test:e2e" +======= + "heroku-prebuild": "npm install bower gulp --save", + "postinstall": "bower install && npm run build-all", + "start": "node server.js", + "build-all": "gulp inject index --gulpfile ./heroku.gulpfile.js", + "test": "gulp test:unit" +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }, "repository": { "type": "git", @@ -36,6 +48,7 @@ }, "homepage": "https://github.com/Four51/OrderCloud-Seed-AngularJS#readme", "dependencies": { +<<<<<<< HEAD "bower": "1.7.7", "browser-sync": "^2.11.1", "del": "^2.2.0", @@ -77,3 +90,45 @@ }, "devDependencies": {} } +======= + "browser-sync": "2.11.1", + "del": "2.2.0", + "express": "4.13.4", + "fs": "0.0.2", + "gulp-angular-filesort": "1.1.1", + "gulp-angular-templatecache": "1.8.0", + "gulp-autoprefixer": "3.1.0", + "gulp-beautify": "2.0.0", + "gulp-cached": "1.1.0", + "gulp-concat": "2.6.0", + "gulp-csso": "1.0.1", + "gulp-filter": "3.0.1", + "gulp-flatten": "0.2.0", + "gulp-htmlmin": "1.3.0", + "gulp-imagemin": "2.4.0", + "gulp-inject": "3.0.0", + "gulp-less": "3.0.5", + "gulp-less-import": "1.0.0", + "gulp-ng-annotate": "1.1.0", + "gulp-ng-constant": "1.1.0", + "gulp-nodemon": "2.0.6", + "gulp-rev": "6.0.1", + "gulp-sourcemaps": "1.6.0", + "gulp-uglify": "1.5.1", + "gulp-wrapper": "1.0.0", + "main-bower-files": "2.11.1", + "require-dir": "0.3.0", + "yargs": "6.6.0" + }, + "devDependencies": { + "chromedriver": "2.26.1", + "gulp-protractor": "3.0.0", + "jasmine-core": "2.5.2", + "karma": "1.3.0", + "karma-jasmine": "1.1.0", + "karma-mocha-reporter": "2.2.1", + "karma-ng-html2js-preprocessor": "1.0.0", + "karma-phantomjs-launcher": "1.0.2" + } +} +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 diff --git a/protractor.conf.js b/protractor.conf.js new file mode 100644 index 00000000..1e6011c0 --- /dev/null +++ b/protractor.conf.js @@ -0,0 +1,27 @@ +var webdriver = require('chromedriver'), + config = require('./gulp.config'); + +exports.config = { + framework: 'jasmine', + directConnect: true, + baseUrl: 'http://localhost:3000', + chromeDriver: webdriver.path, + capabilities: { + 'browserName': 'chrome' + }, + + // Spec patterns are relative to the configuration file location passed + // to protractor (in this example conf.js). + // They may include glob patterns. + specs: [ + config.root + '/src/**/*.test.js', + config.root + '/../Components/**/*.test.js' + ], + + suites: { + all: [ + config.root + '/src/**/*.test.js', + config.root + '/../Components/**/*.test.js' + ] + } +}; \ No newline at end of file diff --git a/server.js b/server.js index 13d84feb..2abd01a9 100644 --- a/server.js +++ b/server.js @@ -16,10 +16,19 @@ switch(env) { break; default: console.log('*** DEV ***'); +<<<<<<< HEAD app.use(express.static(config.root + config.build.replace('.', ''))); app.use(express.static(config.root + config.src.replace('.', '') + 'app/')); app.use(express.static(config.root)); app.use(express.static(config.root + config.components.dir)); +======= + // Host bower_files + app.use('/bower_files', express.static(config.root + config.bowerFiles.replace('.', ''))); + // Host unminfied javascript files + app.use(express.static(config.root + config.build.replace('.', ''))); + // Host unchanged html files + app.use(express.static(config.root + config.src.replace('.', '') + 'app/')); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 app.get('/*', function(req, res) { res.sendFile(config.root + config.build.replace('.', '') + 'index.html'); }); diff --git a/src/README.md b/src/README.md index 78fbf9aa..ae4adef7 100644 --- a/src/README.md +++ b/src/README.md @@ -8,12 +8,25 @@ tests of such code. ``` src/ |- app/ +<<<<<<< HEAD | |- home/ | |- app.js | |- app.spec.js | |- global.less | |- variables.less |- assets/ +======= + | |- common/ + | |- styles/ + | |- app.config.js + | |- app.constants.json + | |- app.controller.js + | |- app.module.js + | |- app.run.js + | |- app.spec.js + |- assets/ + | |- images/ +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 |- index.html ``` diff --git a/src/app/README.md b/src/app/README.md index e0db0fa9..6ec439ab 100644 --- a/src/app/README.md +++ b/src/app/README.md @@ -5,14 +5,26 @@ ``` src/ |- app/ +<<<<<<< HEAD | |- home/ | |- about/ | |- app.js +======= + | |- app.config.js + | |- app.constants.json + | |- app.controller.js + | |- app.module.js + | |- app.run.js +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 | |- app.spec.js ``` The `src/app` directory contains all code specific to this application. Apart +<<<<<<< HEAD from `app.js` and its accompanying tests (discussed below), this directory is +======= +from `app.*.js` and its accompanying tests (discussed below), this directory is +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 filled with subdirectories corresponding to high-level sections of the application, often corresponding to top-level routes. Each directory can have as many subdirectories as it needs, and the build system will understand what to @@ -22,9 +34,15 @@ route `/products`, though this is in no way enforced. Products may then have subdirectories for "create", "view", "search", etc. The "view" submodule may then define a route of `/products/:id`, ad infinitum. +<<<<<<< HEAD ## `app.js` This is our main app configuration file. It kickstarts the whole process by +======= +## `app.module.js` + +This is our main app file. It kickstarts the whole process by +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 requiring all the modules that we need. By default, the OrderCloud AngularJS Seed includes a few useful modules written @@ -50,9 +68,17 @@ angular.module('orderCloud', [ ]) ``` +<<<<<<< HEAD With app modules broken down in this way, all routing is performed by the submodules we include, as that is where our app's functionality is really defined. So all we need to do in `app.js` is specify a default route to follow, +======= +## `app.config.js` + +With app modules broken down in this way, all routing is performed by the +submodules we include, as that is where our app's functionality is really +defined. So all we need to do in `app.config.js` is specify a default route to follow, +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 which route of course is defined in a submodule. In this case, our `home` module is where we want to start, which has a defined route for `/home` in `src/app/home/home.js`. @@ -63,6 +89,11 @@ is where we want to start, which has a defined route for `/home` in }) ``` +<<<<<<< HEAD +======= +## `app.run.js` + +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 Use the main applications run method to execute any code after services have been instantiated. @@ -71,6 +102,11 @@ have been instantiated. }) ``` +<<<<<<< HEAD +======= +## `app.controller.js` + +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 And then we define our main application controller. This is a good place for logic not specific to the template or route, such as menu logic or page title wiring. @@ -88,13 +124,20 @@ not specific to the template or route, such as menu logic or page title wiring. One of the design philosophies of `OrderCloud-Seed-AngularJS` is that tests should exist alongside the code they test and that the build system should be smart enough to +<<<<<<< HEAD know the difference and react accordingly. As such, the unit test for `app.js` +======= +know the difference and react accordingly. As such, the unit test for `app.*.js` +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 is `app.spec.js`, though it is quite minimal. ### Global application styles +<<<<<<< HEAD By default, we include [Ambient](http://ionlyseespots.github.io/ambient-design/index.html) which is an internally developed design framework that makes use of HTML5 elements & CSS3 attributes to layout the document outline. +======= +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 Within the `src/app/` directory we included a `global.less` and `variables.less` file. These should be utilized for application wide LESS variables and mixins. Each component within `src/app` will have a corresponding `less/` directory with a similar structure. diff --git a/src/app/account/account.js b/src/app/account/account.js index a7584e5e..ac6bebfd 100644 --- a/src/app/account/account.js +++ b/src/app/account/account.js @@ -1,9 +1,17 @@ angular.module('orderCloud') .config(AccountConfig) +<<<<<<< HEAD .controller('AccountCtrl', AccountController) .factory('AccountService', AccountService) .controller('ConfirmPasswordCtrl', ConfirmPasswordController) .controller('ChangePasswordCtrl', ChangePasswordController) +======= + .controller('AccountInfoCtrl', AccountInfoController) + .controller('AccountEditModalCtrl', AccountEditModalController) + .factory('AccountService', AccountService) + .controller('ChangePasswordModalCtrl', ChangePasswordModalController) + .controller('ConfirmPasswordCtrl', ConfirmPasswordController) +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 ; function AccountConfig($stateProvider) { @@ -12,6 +20,7 @@ function AccountConfig($stateProvider) { parent: 'base', url: '/account', templateUrl: 'account/templates/account.tpl.html', +<<<<<<< HEAD controller: 'AccountCtrl', controllerAs: 'account' }) @@ -20,11 +29,22 @@ function AccountConfig($stateProvider) { templateUrl: 'account/templates/changePassword.tpl.html', controller: 'ChangePasswordCtrl', controllerAs: 'changePassword' +======= + controller: 'AccountInfoCtrl', + controllerAs: 'accountInfo', + data: { + pageTitle: "Account" + } +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }) ; } +<<<<<<< HEAD function AccountService($q, $uibModal, OrderCloud) { +======= +function AccountService($q, $uibModal, OrderCloud, toastr) { +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 var service = { Update: _update, ChangePassword: _changePassword @@ -48,15 +68,27 @@ function AccountService($q, $uibModal, OrderCloud) { templateUrl: 'account/templates/confirmPassword.modal.tpl.html', controller: 'ConfirmPasswordCtrl', controllerAs: 'confirmPassword', +<<<<<<< HEAD size: 'sm' +======= + size: 'md' +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }).result.then(function(password) { var checkPasswordCredentials = { Username: currentProfile.Username, Password: password }; +<<<<<<< HEAD OrderCloud.Auth.GetToken(checkPasswordCredentials) .then(function() { updateUser(); +======= + + OrderCloud.Auth.GetToken(checkPasswordCredentials) + .then(function() { + updateUser(); + toastr.success('Account changes were saved.', 'Success!'); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }) .catch(function(ex) { deferred.reject(ex); @@ -98,9 +130,56 @@ function AccountService($q, $uibModal, OrderCloud) { return service; } +<<<<<<< HEAD function AccountController($exceptionHandler, toastr, AccountService, CurrentUser) { var vm = this; vm.profile = angular.copy(CurrentUser); +======= +function AccountInfoController($uibModal, CurrentUser){ + var vm = this; + vm.profile = angular.copy(CurrentUser); + vm.currentUser = CurrentUser; + + vm.editInfo = function(){ + $uibModal.open({ + animation: true, + templateUrl: 'account/templates/accountSettings.modal.tpl.html', + controller: 'AccountEditModalCtrl', + controllerAs: 'accountEditModal', + backdrop:'static', + size: 'md', + resolve: { + Profile: function(){ + return vm.profile; + }, + CurrentUser: function(){ + return vm.currentUser; + } + } + }); + }; + + vm.changePassword = function(user){ + $uibModal.open({ + animation: true, + templateUrl: 'account/templates/changePassword.modal.tpl.html', + controller: 'ChangePasswordModalCtrl', + controllerAs: 'changePasswordModal', + backdrop:'static', + size: 'md', + resolve: { + CurrentUser: function(){ + return user; + } + } + }); + }; +} + +function AccountEditModalController($uibModalInstance, $exceptionHandler, AccountService, CurrentUser, Profile){ + var vm = this; + vm.profile = Profile; +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 var currentProfile = CurrentUser; vm.update = function() { @@ -108,11 +187,19 @@ function AccountController($exceptionHandler, toastr, AccountService, CurrentUse .then(function(data) { vm.profile = angular.copy(data); currentProfile = data; +<<<<<<< HEAD toastr.success('Account changes were saved.', 'Success!'); }) .catch(function(ex) { vm.profile = currentProfile; $exceptionHandler(ex) +======= + vm.submit(); + }) + .catch(function(ex) { + vm.profile = currentProfile; + $exceptionHandler(ex); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }); }; @@ -120,6 +207,7 @@ function AccountController($exceptionHandler, toastr, AccountService, CurrentUse vm.profile = currentProfile; form.$setPristine(true); }; +<<<<<<< HEAD } function ConfirmPasswordController($uibModalInstance) { @@ -127,6 +215,11 @@ function ConfirmPasswordController($uibModalInstance) { vm.submit = function() { $uibModalInstance.close(vm.password); +======= + + vm.submit = function() { + $uibModalInstance.close(); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }; vm.cancel = function() { @@ -134,7 +227,11 @@ function ConfirmPasswordController($uibModalInstance) { }; } +<<<<<<< HEAD function ChangePasswordController($state, $exceptionHandler, toastr, AccountService, CurrentUser) { +======= +function ChangePasswordModalController(toastr, $state, $exceptionHandler, AccountService, $uibModalInstance, CurrentUser){ +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 var vm = this; vm.currentUser = CurrentUser; @@ -145,6 +242,7 @@ function ChangePasswordController($state, $exceptionHandler, toastr, AccountServ vm.currentUser.CurrentPassword = null; vm.currentUser.NewPassword = null; vm.currentUser.ConfirmPassword = null; +<<<<<<< HEAD $state.go('account'); }) .catch(function(ex) { @@ -152,3 +250,33 @@ function ChangePasswordController($state, $exceptionHandler, toastr, AccountServ }); }; } +======= + vm.submit(); + $state.go('account.information'); + }) + .catch(function(ex) { + $exceptionHandler(ex); + }); + }; + + vm.submit = function() { + $uibModalInstance.close(); + }; + + vm.cancel = function() { + $uibModalInstance.dismiss('cancel'); + }; +} + +function ConfirmPasswordController($uibModalInstance) { + var vm = this; + + vm.submit = function() { + $uibModalInstance.close(vm.password); + }; + + vm.cancel = function() { + $uibModalInstance.dismiss('cancel'); + }; +} +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 diff --git a/src/app/account/templates/account.tpl.html b/src/app/account/templates/account.tpl.html index 51f08221..cbcad4c1 100644 --- a/src/app/account/templates/account.tpl.html +++ b/src/app/account/templates/account.tpl.html @@ -1,3 +1,4 @@ +<<<<<<< HEAD
@@ -31,4 +32,48 @@
-
\ No newline at end of file + +======= +
+ +
+

My Account

+
+
+
+
+
+
Name: {{accountInfo.profile.FirstName + ' ' + accountInfo.profile.LastName}}
+
Email Address: {{accountInfo.profile.Email}}
+
Username: {{accountInfo.profile.Username}}
+
Phone Number: {{accountInfo.profile.Phone}}
+
+
+
+ + +
+
+
+
+
+
+>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 diff --git a/src/app/account/templates/accountSettings.modal.tpl.html b/src/app/account/templates/accountSettings.modal.tpl.html new file mode 100644 index 00000000..eea319d9 --- /dev/null +++ b/src/app/account/templates/accountSettings.modal.tpl.html @@ -0,0 +1,39 @@ +
+ + + +
diff --git a/src/app/account/templates/changePassword.modal.tpl.html b/src/app/account/templates/changePassword.modal.tpl.html new file mode 100644 index 00000000..32b9e297 --- /dev/null +++ b/src/app/account/templates/changePassword.modal.tpl.html @@ -0,0 +1,27 @@ +
+ + + +
diff --git a/src/app/account/templates/confirmPassword.modal.tpl.html b/src/app/account/templates/confirmPassword.modal.tpl.html index c3791fad..9a671667 100644 --- a/src/app/account/templates/confirmPassword.modal.tpl.html +++ b/src/app/account/templates/confirmPassword.modal.tpl.html @@ -1,3 +1,4 @@ +<<<<<<< HEAD +======= +
+ + + +
+>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 diff --git a/src/app/account/tests/account.spec.js b/src/app/account/tests/account.spec.js index 7d0a210f..102c4c76 100644 --- a/src/app/account/tests/account.spec.js +++ b/src/app/account/tests/account.spec.js @@ -2,7 +2,12 @@ describe('Component: Account', function() { var scope, q, account, +<<<<<<< HEAD accountFactory; +======= + accountFactory, + uibModalInstance; +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 beforeEach(module('orderCloud')); beforeEach(module('orderCloud.sdk')); beforeEach(inject(function($q, $rootScope, AccountService) { @@ -19,6 +24,10 @@ describe('Component: Account', function() { Active: true }; accountFactory = AccountService; +<<<<<<< HEAD +======= + uibModalInstance = jasmine.createSpyObj('modalInstance', ['close', 'dismiss', 'result.then']); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 })); describe('Factory: AccountService', function() { @@ -72,6 +81,7 @@ describe('Component: Account', function() { }); }); +<<<<<<< HEAD describe('Controller: AccountCtrl', function() { var accountCtrl, currentProfile; beforeEach(inject(function($state, $controller) { @@ -155,11 +165,113 @@ describe('Component: Account', function() { changePasswordCtrl = $controller('ChangePasswordCtrl', { $scope: scope, CurrentUser: {} +======= + describe('Controller: AccountInfoCtrl', function() { + var accountInfoCtrl, + uibModal, + actualOptions; + beforeEach(inject(function ($uibModal, $controller) { + accountInfoCtrl = $controller('AccountInfoCtrl', { + CurrentUser: {}, + Profile: {} + }); + uibModal = $uibModal; + accountInfoCtrl.editInfoModalOptions = { + animation: true, + templateUrl: 'account/templates/accountSettings.modal.tpl.html', + controller: 'AccountEditModalCtrl', + controllerAs: 'accountEditModal', + backdrop: 'static', + size: 'md', + resolve: { + Profile: jasmine.any(Function), + CurrentUser: jasmine.any(Function) + } + }; + accountInfoCtrl.changePasswordModalOptions = { + animation: true, + templateUrl: 'account/templates/changePassword.modal.tpl.html', + controller: 'ChangePasswordModalCtrl', + controllerAs: 'changePasswordModal', + backdrop:'static', + size: 'md', + resolve: { + CurrentUser: jasmine.any(Function) + } + }; + })); + describe('editInfo', function() { + it('should call the $uibModal open with editInfo modal', function() { + spyOn(uibModal, 'open').and.callFake(function(options) { + actualOptions = options; + return uibModalInstance; + }); + accountInfoCtrl.editInfo(); + expect(uibModal.open).toHaveBeenCalledWith(accountInfoCtrl.editInfoModalOptions); + }); + }); + describe('changePassword', function() { + it('should call the $uibModal open with changePassword modal', function() { + spyOn(uibModal, 'open').and.callFake(function(options) { + actualOptions = options; + return uibModalInstance; + }); + accountInfoCtrl.changePassword(); + expect(uibModal.open).toHaveBeenCalledWith(accountInfoCtrl.changePasswordModalOptions); + }); + }); + }); + + describe('Controller: AccountEditModalCtrl', function() { + var accountEditModalCtrl, currentProfile; + beforeEach(inject(function($state, $controller) { + accountEditModalCtrl = $controller('AccountEditModalCtrl', { + $uibModalInstance: uibModalInstance, + CurrentUser: {}, + Profile: {} + }); + })); + describe('update', function() { + beforeEach(inject(function() { + currentProfile = {}; + accountEditModalCtrl.Profile = {}; + var defer = q.defer(); + defer.resolve(); + spyOn(accountFactory, 'Update').and.returnValue(defer.promise); + accountEditModalCtrl.update(); + })); + it('should call the Accounts Update method', inject(function() { + expect(accountFactory.Update).toHaveBeenCalledWith(currentProfile, accountEditModalCtrl.Profile); + })); + }); + describe('submit', function() { + it('should close the modal', function() { + accountEditModalCtrl.submit(); + expect(uibModalInstance.close).toHaveBeenCalled(); + }); + }); + describe('cancel', function() { + it('should dismiss the modal', function() { + accountEditModalCtrl.cancel(); + expect(uibModalInstance.dismiss).toHaveBeenCalled(); + }); + }); + }); + + describe('Controller: ChangePasswordModalCtrl', function () { + var changePasswordModalCtrl; + beforeEach(inject(function($state, $controller) { + changePasswordModalCtrl = $controller('ChangePasswordModalCtrl', { + $scope: scope, + CurrentUser: {}, + $uibModalInstance: uibModalInstance +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }); spyOn($state, 'go').and.returnValue(true); })); describe('changePassword', function() { beforeEach(inject(function() { +<<<<<<< HEAD changePasswordCtrl.currentUser = account; var defer = q.defer(); defer.resolve(account); @@ -171,6 +283,51 @@ describe('Component: Account', function() { })); }); +======= + changePasswordModalCtrl.currentUser = account; + var defer = q.defer(); + defer.resolve(account); + spyOn(accountFactory, 'ChangePassword').and.returnValue(defer.promise); + changePasswordModalCtrl.changePassword(); + })); + it ('should call the Accounts ChangePassword method', inject(function() { + expect(accountFactory.ChangePassword).toHaveBeenCalledWith(changePasswordModalCtrl.currentUser); + })); + }); + describe('submit', function() { + it('should close the modal', function() { + changePasswordModalCtrl.submit(); + expect(uibModalInstance.close).toHaveBeenCalled(); + }); + }); + describe('cancel', function() { + it('should dismiss the modal', function(){ + changePasswordModalCtrl.cancel(); + expect(uibModalInstance.dismiss).toHaveBeenCalled(); + }); + }); + }); + describe('Controller: ConfirmPasswordCtrl', function () { + var confirmPasswordCtrl; + beforeEach(inject(function($controller) { + confirmPasswordCtrl = $controller('ConfirmPasswordCtrl', { + $uibModalInstance: uibModalInstance, + password: 'fakepassword' + }); + })); + describe('submit', function() { + it('should close the modal after confirming password', function() { + confirmPasswordCtrl.submit(); + expect(uibModalInstance.close).toHaveBeenCalledWith(confirmPasswordCtrl.password); + }); + }); + describe('cancel', function() { + it('should dismiss the modal', function() { + confirmPasswordCtrl.cancel(); + expect(uibModalInstance.dismiss).toHaveBeenCalled(); + }); + }); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }); }); diff --git a/src/app/addPromotion/addPromotion.js b/src/app/addPromotion/addPromotion.js new file mode 100644 index 00000000..1afffa8c --- /dev/null +++ b/src/app/addPromotion/addPromotion.js @@ -0,0 +1,22 @@ +angular.module('orderCloud') + .component('ocAddPromotion', { + templateUrl: 'addPromotion/templates/addPromotion.tpl.html', + bindings: { + order: '<' + }, + controller: AddPromotionComponentCtrl + }); + +function AddPromotionComponentCtrl($exceptionHandler, $rootScope, OrderCloud, toastr) { + this.submit = function(orderID, promoCode) { + OrderCloud.Orders.AddPromotion(orderID, promoCode) + .then(function(promo) { + $rootScope.$broadcast('OC:UpdatePromotions', orderID); + $rootScope.$broadcast('OC:UpdateOrder', orderID); + toastr.success('Promo code '+ promo.Code + ' added!', 'Success'); + }) + .catch(function(err) { + $exceptionHandler(err); + }); + }; +} \ No newline at end of file diff --git a/src/app/addPromotion/templates/addPromotion.tpl.html b/src/app/addPromotion/templates/addPromotion.tpl.html new file mode 100644 index 00000000..efea60e3 --- /dev/null +++ b/src/app/addPromotion/templates/addPromotion.tpl.html @@ -0,0 +1,9 @@ +
+ +
+ + + + +
+
\ No newline at end of file diff --git a/src/app/addPromotion/tests/addPromotion.spec.js b/src/app/addPromotion/tests/addPromotion.spec.js new file mode 100644 index 00000000..5f7a55d0 --- /dev/null +++ b/src/app/addPromotion/tests/addPromotion.spec.js @@ -0,0 +1,50 @@ +describe('Component: addPromotion', function(){ + var scope, + rootScope, + q, + oc + ; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($rootScope, $q, OrderCloud){ + scope = $rootScope.$new(); + rootScope = $rootScope; + q = $q; + oc = OrderCloud; + })); + describe('Component Directive: ocAddPromotion', function(){ + var addPromotionCtrl, + toaster + ; + beforeEach(inject(function($componentController, toastr){ + toaster = toastr; + addPromotionCtrl = $componentController('ocAddPromotion', { + $scope: scope, + $rootScope: rootScope, + OrderCloud: oc, + toastr: toaster + }); + })); + describe('submit', function(){ + var mockPromo, + mockOrderID; + beforeEach(function(){ + mockPromo = {Code:'Discount10'}; + mockOrderID = 'order123'; + + var defer = q.defer(); + defer.resolve(mockPromo); + spyOn(oc.Orders, 'AddPromotion').and.returnValue(defer.promise); + spyOn(rootScope, '$broadcast'); + spyOn(toaster, 'success'); + addPromotionCtrl.submit(mockOrderID, mockPromo.Code); + }); + it('should call Orders.AddPromotion', function(){ + expect(oc.Orders.AddPromotion).toHaveBeenCalledWith(mockOrderID, mockPromo.Code); + scope.$digest(); + expect(rootScope.$broadcast).toHaveBeenCalledWith('OC:UpdatePromotions', mockOrderID); + expect(toaster.success).toHaveBeenCalledWith('Promo code ' + mockPromo.Code + ' added!', 'Success'); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/app.config.js b/src/app/app.config.js new file mode 100644 index 00000000..f28d1183 --- /dev/null +++ b/src/app/app.config.js @@ -0,0 +1,38 @@ +angular.module('orderCloud') + .config(AppConfig) +; + +function AppConfig($urlRouterProvider, $urlMatcherFactoryProvider, $locationProvider, defaultstate, $qProvider, $provide, $httpProvider) { + //Routing + $locationProvider.html5Mode(true); + $urlMatcherFactoryProvider.strictMode(false); + $urlRouterProvider.otherwise(function ($injector) { + var $state = $injector.get('$state'); + $state.go(defaultstate); //Set the default state name in app.constants.json + }); + + //Error Handling + $provide.decorator('$exceptionHandler', handler); + $qProvider.errorOnUnhandledRejections(false); //Stop .catch validation from angular v1.6.0 + function handler($delegate, $injector) { //Catch all for unhandled errors + return function(ex, cause) { + $delegate(ex, cause); + $injector.get('toastr').error(ex.data ? (ex.data.error || (ex.data.Errors ? ex.data.Errors[0].Message : ex.data)) : ex.message, 'Error'); + }; + } + + //HTTP Interceptor for OrderCloud Authentication + $httpProvider.interceptors.push(function($q, $rootScope) { + return { + 'responseError': function(rejection) { + if (rejection.config.url.indexOf('ordercloud.io') > -1 && rejection.status == 401) { + $rootScope.$broadcast('OC:AccessInvalidOrExpired'); //Trigger RememberMe || AuthAnonymous in AppCtrl + } + if (rejection.config.url.indexOf('ordercloud.io') > -1 && rejection.status == 403){ + $rootScope.$broadcast('OC:AccessForbidden'); //Trigger warning toastr message for insufficient permissions + } + return $q.reject(rejection); + } + }; + }); +} \ No newline at end of file diff --git a/src/app/app.constants.json b/src/app/app.constants.json new file mode 100644 index 00000000..ea190d0d --- /dev/null +++ b/src/app/app.constants.json @@ -0,0 +1,10 @@ +{ + "appname": "Angular Buyer", + "scope": "FullAccess", + "clientid": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "buyerid": "XXXX", + "catalogid": "XXXX", + "environment": "prod", + "defaultstate": "home", + "anonymous": false +} \ No newline at end of file diff --git a/src/app/app.controller.js b/src/app/app.controller.js new file mode 100644 index 00000000..d53155a6 --- /dev/null +++ b/src/app/app.controller.js @@ -0,0 +1,57 @@ +angular.module('orderCloud') + .controller('AppCtrl', AppController) +; + +function AppController($q, $rootScope, $state, $ocMedia, toastr, LoginService, appname, anonymous, defaultstate) { + var vm = this; + vm.name = appname; + vm.$state = $state; + vm.$ocMedia = $ocMedia; + vm.stateLoading = undefined; + + function cleanLoadingIndicators() { + if (vm.stateLoading && vm.stateLoading.promise && !vm.stateLoading.promise.$cgBusyFulfilled) vm.stateLoading.resolve(); //resolve leftover loading promises + } + + //Detect if the app was loaded on a touch device with relatively good certainty + //http://stackoverflow.com/a/6262682 + vm.isTouchDevice = (function() { + var el = document.createElement('div'); + el.setAttribute('ongesturestart', 'return;'); // or try "ontouchstart" + return typeof el.ongesturestart === "function"; + })(); + + vm.logout = function() { + LoginService.Logout(); + }; + + $rootScope.$on('$stateChangeStart', function(e, toState) { + cleanLoadingIndicators(); + var defer = $q.defer(); + if (toState.data) defer.message = toState.data.loadingMessage; + vm.stateLoading = defer; + }); + + $rootScope.$on('$stateChangeSuccess', function() { + cleanLoadingIndicators(); + }); + + $rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) { + if (toState.name == defaultstate) event.preventDefault(); //prevent infinite loop when error occurs on default state (otherwise in Routing config) + cleanLoadingIndicators(); + console.log(error); + }); + + $rootScope.$on('OC:AccessInvalidOrExpired', function() { + if (anonymous) { + cleanLoadingIndicators(); + LoginService.AuthAnonymous(); + } else { + LoginService.RememberMe(); + } + }); + + $rootScope.$on('OC:AccessForbidden', function(){ + toastr.warning("You do not have permission to access this page."); + }); +} \ No newline at end of file diff --git a/src/app/app.module.js b/src/app/app.module.js new file mode 100644 index 00000000..5dbf7981 --- /dev/null +++ b/src/app/app.module.js @@ -0,0 +1,17 @@ +angular.module('orderCloud', [ + 'ngSanitize', + 'ngAnimate', + 'ngMessages', + 'ngTouch', + 'ui.tree', + 'ui.router', + 'ui.bootstrap', + 'orderCloud.sdk', + 'LocalForageModule', + 'toastr', + 'angular-busy', + 'jcs-autoValidate', + 'treeControl', + 'hl.sticky' + ] +); \ No newline at end of file diff --git a/src/app/app.run.js b/src/app/app.run.js new file mode 100644 index 00000000..a784fefe --- /dev/null +++ b/src/app/app.run.js @@ -0,0 +1,24 @@ +angular.module('orderCloud') + .run(AppRun) +; + +function AppRun(OrderCloud, catalogid, uibDatepickerConfig, uibDatepickerPopupConfig, defaultErrorMessageResolver) { + //Set Default CatalogID + catalogid ? OrderCloud.CatalogID.Set(catalogid) : OrderCloud.CatalogID.Set(OrderCloud.BuyerID.Get()); + + //Default Datepicker Options + uibDatepickerConfig.showWeeks = false; + uibDatepickerPopupConfig.showButtonBar = false; + + //Set Custom Error Messages for angular-auto-validate --- http://jonsamwell.github.io/angular-auto-validate/ --- + defaultErrorMessageResolver.getErrorMessages().then(function (errorMessages) { + errorMessages['customPassword'] = 'Password must be at least eight characters long and include at least one letter and one number'; + //regex for customPassword = ^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!$%@#£€*?&]{8,}$ + errorMessages['positiveInteger'] = 'Please enter a positive integer'; + //regex positiveInteger = ^[0-9]*[1-9][0-9]*$ + errorMessages['ID_Name'] = 'Only Alphanumeric characters, hyphens and underscores are allowed'; + //regex ID_Name = ([A-Za-z0-9\-\_]+) + errorMessages['confirmpassword'] = 'Your passwords do not match'; + errorMessages['noSpecialChars'] = 'Only Alphanumeric characters are allowed'; + }); +} \ No newline at end of file diff --git a/src/app/base/base.js b/src/app/base/base.js index 40debf94..a9a89f96 100644 --- a/src/app/base/base.js +++ b/src/app/base/base.js @@ -1,6 +1,7 @@ angular.module('orderCloud') .config(BaseConfig) .controller('BaseCtrl', BaseController) +<<<<<<< HEAD .filter('occomponents', occomponents) ; @@ -164,3 +165,114 @@ function occomponents() { return result; } } +======= + .factory('NewOrder', NewOrderService) +; + +function BaseConfig($stateProvider) { + $stateProvider.state('base', { + url: '', + abstract: true, + views: { + '': { + templateUrl: 'base/templates/base.tpl.html', + controller: 'BaseCtrl', + controllerAs: 'base' + }, + 'nav@base': { + 'templateUrl': 'base/templates/navigation.tpl.html' + } + }, + resolve: { + CurrentUser: function($q, $state, OrderCloud, buyerid) { + return OrderCloud.Me.Get() + .then(function(data) { + OrderCloud.BuyerID.Set(buyerid); + return data; + }) + }, + ExistingOrder: function($q, OrderCloud, CurrentUser) { + return OrderCloud.Me.ListOutgoingOrders(null, 1, 1, null, "!DateCreated", {Status:"Unsubmitted"}) + .then(function(data) { + return data.Items[0]; + }); + }, + CurrentOrder: function(ExistingOrder, NewOrder, CurrentUser) { + if (!ExistingOrder) { + return NewOrder.Create({}); + } else { + return ExistingOrder; + } + }, + AnonymousUser: function($q, OrderCloud, CurrentUser) { + CurrentUser.Anonymous = angular.isDefined(JSON.parse(atob(OrderCloud.Auth.ReadToken().split('.')[1])).orderid); + } + } + }); +} + +function BaseController($rootScope, $state, ProductSearch, CurrentUser, CurrentOrder, OrderCloud) { + var vm = this; + vm.currentUser = CurrentUser; + vm.currentOrder = CurrentOrder; + + vm.mobileSearch = function() { + ProductSearch.Open() + .then(function(data) { + if (data.productID) { + $state.go('productDetail', {productid: data.productID}); + } else { + $state.go('productSearchResults', {searchTerm: data.searchTerm}); + } + }); + }; + + $rootScope.$on('OC:UpdateOrder', function(event, OrderID, message) { + vm.orderLoading = { + message: message + }; + vm.orderLoading.promise = OrderCloud.Orders.Get(OrderID) + .then(function(data) { + vm.currentOrder = data; + }); + }); +} + +function NewOrderService($q, OrderCloud) { + var service = { + Create: _create + }; + + function _create() { + var deferred = $q.defer(); + var order = {}; + + //ShippingAddressID + OrderCloud.Me.ListAddresses(null, 1, 100, null, null, {Shipping: true}) + .then(function(shippingAddresses) { + if (shippingAddresses.Items.length) order.ShippingAddressID = shippingAddresses.Items[0].ID; + setBillingAddress(); + }); + + //BillingAddressID + function setBillingAddress() { + OrderCloud.Me.ListAddresses(null, 1, 100, null, null, {Billing: true}) + .then(function(billingAddresses) { + if (billingAddresses.Items.length) order.BillingAddressID = billingAddresses.Items[0].ID; + createOrder(); + }); + } + + function createOrder() { + OrderCloud.Orders.Create(order) + .then(function(order) { + deferred.resolve(order); + }); + } + + return deferred.promise; + } + + return service; +} +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 diff --git a/src/app/base/templates/base.tpl.html b/src/app/base/templates/base.tpl.html index 33e0ec5e..6161c00f 100644 --- a/src/app/base/templates/base.tpl.html +++ b/src/app/base/templates/base.tpl.html @@ -1,3 +1,4 @@ +<<<<<<< HEAD
@@ -6,3 +7,9 @@ +======= +
+ +
+
+>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 diff --git a/src/app/base/templates/navigation.tpl.html b/src/app/base/templates/navigation.tpl.html new file mode 100644 index 00000000..beb1ffc1 --- /dev/null +++ b/src/app/base/templates/navigation.tpl.html @@ -0,0 +1,42 @@ +
+
+
+ + + +
+ +
+ + +
+ + + +
+
diff --git a/src/app/base/tests/base.spec.js b/src/app/base/tests/base.spec.js index feb65e81..00e82c6b 100644 --- a/src/app/base/tests/base.spec.js +++ b/src/app/base/tests/base.spec.js @@ -2,6 +2,7 @@ describe('Component: Base', function() { var q, scope, oc, +<<<<<<< HEAD underscore; beforeEach(module('orderCloud')); beforeEach(module('orderCloud.sdk')); @@ -48,6 +49,52 @@ describe('Component: Base', function() { expect(components.nonSpecific).not.toBe(null); expect(components.buyerSpecific).not.toBe(null); })); +======= + buyerid = "BUYERID", + state, + injector; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(module('ui.router')); + beforeEach(inject(function($q, $rootScope, $state, OrderCloud, $injector) { + q = $q; + scope = $rootScope.$new(); + oc = OrderCloud; + state = $state; + injector = $injector; + })); + describe('State: Base', function() { + var base; + beforeEach(function() { + base = state.get('base'); + }); + it ('should attempt to get the current user and set the buyerid', function() { + var user = q.defer(); + user.resolve('TEST USER'); + spyOn(oc.Me, 'Get').and.returnValue(user.promise); + spyOn(oc.BuyerID, 'Set').and.callThrough(); + injector.invoke(base.resolve.CurrentUser, scope, {$q:q, $state:state, OrderCloud:oc, buyerid:buyerid}); + expect(oc.Me.Get).toHaveBeenCalled(); + scope.$digest(); + expect(oc.BuyerID.Set).toHaveBeenCalledWith('BUYERID'); + }); + it ('should search for an existing unsubmitted order', function() { + var orderList = q.defer(); + orderList.resolve({Items:['TEST ORDER']}); + spyOn(oc.Me, 'ListOutgoingOrders').and.returnValue(orderList.promise); + var currentUser = injector.invoke(base.resolve.CurrentUser); + injector.invoke(base.resolve.ExistingOrder, scope, {$q:q, OrderCloud:oc, CurrentUser:currentUser}); + expect(oc.Me.ListOutgoingOrders).toHaveBeenCalledWith(null, 1, 1, null, "!DateCreated", {Status: "Unsubmitted"}); + }); + it ('should create a new order if there is not an existing unsubmitted order', inject(function(NewOrder) { + var newOrder = NewOrder, + existingOrder, //undefined existing order + currentUser = injector.invoke(base.resolve.CurrentUser); + spyOn(newOrder, 'Create'); + injector.invoke(base.resolve.CurrentOrder, scope, {ExistingOrder: existingOrder, NewOrder: newOrder, CurrentUser: currentUser}); + expect(newOrder.Create).toHaveBeenCalledWith({}); + })) +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }); describe('Controller: BaseCtrl', function(){ @@ -55,6 +102,7 @@ describe('Component: Base', function() { fake_user = { Username: 'notarealusername', Password: 'notarealpassword' +<<<<<<< HEAD }; beforeEach(inject(function($controller) { baseCtrl = $controller('BaseCtrl', { @@ -91,4 +139,21 @@ describe('Component: Base', function() { })); /* No tests needed */ }); +======= + }, + fake_order = { + ID: 'fakeorder' + }; + beforeEach(inject(function($controller) { + baseCtrl = $controller('BaseCtrl', { + CurrentUser: fake_user, + CurrentOrder: fake_order + }); + })); + it ('should initialize the current user and order into its scope', function() { + expect(baseCtrl.currentUser).toBe(fake_user); + expect(baseCtrl.currentOrder).toBe(fake_order); + }); + }); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }); diff --git a/src/app/base/tests/base.test.js b/src/app/base/tests/base.test.js index a31d445d..dc276cd0 100644 --- a/src/app/base/tests/base.test.js +++ b/src/app/base/tests/base.test.js @@ -1,6 +1,10 @@ function BasePage() { this.get = function() { +<<<<<<< HEAD browser.get('/#'); +======= + browser.get('#'); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }; this.getTitle = function() { @@ -17,7 +21,11 @@ describe('Base', function() { describe('base', function() { it ("should display the correct title", function() { +<<<<<<< HEAD expect(page.getTitle()).toBe('OrderCloud'); +======= + expect(page.getTitle()).toBe('Angular Buyer'); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }); }) }); \ No newline at end of file diff --git a/src/app/cart/cart.js b/src/app/cart/cart.js new file mode 100644 index 00000000..3d87cb49 --- /dev/null +++ b/src/app/cart/cart.js @@ -0,0 +1,89 @@ +angular.module('orderCloud') + .config(CartConfig) + .controller('CartCtrl', CartController) +; + +function CartConfig($stateProvider) { + $stateProvider + .state('cart', { + parent: 'base', + url: '/cart', + templateUrl: 'cart/templates/cart.tpl.html', + controller: 'CartCtrl', + controllerAs: 'cart', + data: { + pageTitle: "Shopping Cart" + }, + resolve: { + LineItemsList: function($q, $state, toastr, OrderCloud, ocLineItems, CurrentOrder) { + var dfd = $q.defer(); + OrderCloud.LineItems.List(CurrentOrder.ID) + .then(function(data) { + if (!data.Items.length) { + dfd.resolve(data); + } + else { + ocLineItems.GetProductInfo(data.Items) + .then(function() { + dfd.resolve(data); + }); + } + }) + .catch(function() { + toastr.error('Your order does not contain any line items.', 'Error'); + dfd.reject(); + }); + return dfd.promise; + }, + CurrentPromotions: function(CurrentOrder, OrderCloud) { + return OrderCloud.Orders.ListPromotions(CurrentOrder.ID); + } + } + }); +} + +function CartController($rootScope, $state, toastr, OrderCloud, LineItemsList, CurrentPromotions, ocConfirm) { + var vm = this; + vm.lineItems = LineItemsList; + vm.promotions = CurrentPromotions.Meta ? CurrentPromotions.Items : CurrentPromotions; + vm.removeItem = function(order, scope) { + vm.lineLoading = []; + vm.lineLoading[scope.$index] = OrderCloud.LineItems.Delete(order.ID, scope.lineItem.ID) + .then(function () { + $rootScope.$broadcast('OC:UpdateOrder', order.ID); + vm.lineItems.Items.splice(scope.$index, 1); + toastr.success('Line Item Removed'); + }); + }; + + //TODO: missing unit tests + vm.removePromotion = function(order, scope) { + OrderCloud.Orders.RemovePromotion(order.ID, scope.promotion.Code) + .then(function() { + $rootScope.$broadcast('OC:UpdateOrder', order.ID); + vm.promotions.splice(scope.$index, 1); + }); + }; + + vm.cancelOrder = function(order){ + ocConfirm.Confirm("Are you sure you want cancel this order?") + .then(function() { + OrderCloud.Orders.Delete(order.ID) + .then(function(){ + $state.go("home",{}, {reload:'base'}) + }); + }); + }; + + //TODO: missing unit tests + $rootScope.$on('OC:UpdatePromotions', function(event, orderid) { + OrderCloud.Orders.ListPromotions(orderid) + .then(function(data) { + if (data.Meta) { + vm.promotions = data.Items; + } else { + vm.promotions = data; + } + }); + }); +} diff --git a/src/app/cart/cart.md b/src/app/cart/cart.md new file mode 100644 index 00000000..32bcfe3a --- /dev/null +++ b/src/app/cart/cart.md @@ -0,0 +1,7 @@ +## Cart Component Overview + +This component allows buyer users with an open order to view the items on the order. + +It uses the Orders, and LineItems resources to build up the data. + +Cart is a buyer perspective component, and can only be accessed when logged in as a buyer user, or when impersonating a buyer user. diff --git a/src/app/cart/templates/cart.tpl.html b/src/app/cart/templates/cart.tpl.html new file mode 100644 index 00000000..f35a10b4 --- /dev/null +++ b/src/app/cart/templates/cart.tpl.html @@ -0,0 +1,116 @@ +
+

+
+ + +
+ Shopping Cart +

+
+
You do not have any items in your cart.
+
+ +
+
+ +
+
+
+ {{lineItem.Product.xp.image.Name || 'Product Image'}} +
+
+
+
+
+

+ {{lineItem.Product.Name}} +

+ {{lineItem.ProductID}} +
    +
  • + {{spec.Name}}: + {{spec.Value}} +
  • +
+
+
+
+
+

{{lineItem.UnitPrice | currency}}

+
+
+
+ +
+
+
+

{{lineItem.LineTotal | currency}}

+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/src/app/cart/templates/minicart.tpl.html b/src/app/cart/templates/minicart.tpl.html new file mode 100644 index 00000000..cf81cf38 --- /dev/null +++ b/src/app/cart/templates/minicart.tpl.html @@ -0,0 +1,67 @@ + + \ No newline at end of file diff --git a/src/app/cart/templates/modalMinicart.tpl.html b/src/app/cart/templates/modalMinicart.tpl.html new file mode 100644 index 00000000..6a992b3d --- /dev/null +++ b/src/app/cart/templates/modalMinicart.tpl.html @@ -0,0 +1,49 @@ +
+
+
+
+ + + +
+
+
+ +
diff --git a/src/app/cart/test/cart.spec.js b/src/app/cart/test/cart.spec.js new file mode 100644 index 00000000..cd29583f --- /dev/null +++ b/src/app/cart/test/cart.spec.js @@ -0,0 +1,127 @@ +describe('Component: Cart', function() { + var scope, + q, + oc, + currentOrder, + rootScope, + lineItemsList, + _ocLineItems, + fakeOrder, + user + ; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(module(function($provide) { + $provide.value('CurrentOrder', {ID: "MockOrderID3456"}); + })); + beforeEach(inject(function($rootScope, $q, OrderCloud, ocLineItems, CurrentOrder) { + scope = $rootScope.$new(); + q = $q; + oc = OrderCloud; + _ocLineItems = ocLineItems; + currentOrder = CurrentOrder; + rootScope = $rootScope; + fakeOrder = { + ID: "TestOrder123456789", + Type: "Standard", + FromUserID: "TestUser123456789", + BillingAddressID: "TestAddress123456789", + ShippingAddressID: "TestAddress123456789", + SpendingAccountID: null, + Comments: null, + PaymentMethod: null, + CreditCardID: null, + ShippingCost: null, + TaxCost: null + }; + lineItemsList = { + "Items" : [{ID:"LI1"}, {ID:"LI2"}], + "Meta" : { + "Page": 1, + "PageSize": 20, + "TotalCount":29, + "TotalPages": 3, + "ItemRange" : [1,2] + } + }; + user = { + ID: "TestUser132456789", + xp: { + defaultShippingAddressID: "TestAddress123456789", + defaultBillingAddressID: "TestAddress123456789", + defaultCreditCardID: "creditCard" + } + + }; + })); + + describe('State: Cart', function() { + var state; + beforeEach(inject(function($state) { + state = $state.get('cart'); + var defer = q.defer(); + defer.resolve(lineItemsList); + spyOn(oc.LineItems,'List').and.returnValue(defer.promise); + spyOn(_ocLineItems,'GetProductInfo').and.returnValue(defer.promise); + + })); + it('should call LineItems.List',inject(function($injector){ + $injector.invoke(state.resolve.LineItemsList); + expect(oc.LineItems.List).toHaveBeenCalledWith(currentOrder.ID); + })); + it('should call LineItemHelper', inject(function($injector){ + $injector.invoke(state.resolve.LineItemsList); + scope.$digest(); + expect(_ocLineItems.GetProductInfo).toHaveBeenCalled(); + })); + }); + + describe('Controller : CartController',function() { + var cartController; + var confirm; + beforeEach(inject(function($state, $controller, ocConfirm) { + cartController = $controller('CartCtrl', { + $scope: scope, + CurrentPromotions: [], + LineItemsList: lineItemsList + }); + confirm = ocConfirm; + var defer = q.defer(); + defer.resolve(lineItemsList); + spyOn(rootScope, '$broadcast'); + })); + + describe('removeItem()', function() { + beforeEach(function() { + var df = q.defer(); + df.resolve(); + spyOn(oc.LineItems, 'Delete').and.returnValue(df.promise); + }); + it ('should delete the line item', function() { + cartController.removeItem(fakeOrder, {$index:0, lineItem: lineItemsList.Items[0]}); + expect(oc.LineItems.Delete).toHaveBeenCalledWith(fakeOrder.ID, lineItemsList.Items[0].ID); + scope.$digest(); + expect(rootScope.$broadcast).toHaveBeenCalledWith('OC:UpdateOrder', fakeOrder.ID); + expect(cartController.lineItems.Items).toEqual([{ID:"LI2"}]); + }) + }); + + describe('CancelOrder',function() { + beforeEach(function() { + var df = q.defer(); + df.resolve(); + spyOn(confirm,'Confirm').and.returnValue(df.promise); + spyOn(oc.Orders, 'Delete').and.returnValue(df.promise); + }); + it('should call OrderCloud Confirm modal prompt', function() { + cartController.cancelOrder(); + expect(confirm.Confirm).toHaveBeenCalled(); + }); + it('should call OC Orders Delete Method', function(){ + cartController.cancelOrder(fakeOrder); + scope.$digest(); + expect(oc.Orders.Delete).toHaveBeenCalledWith(fakeOrder.ID); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/categoryBrowse/categoryBrowse.js b/src/app/categoryBrowse/categoryBrowse.js new file mode 100644 index 00000000..b9f33471 --- /dev/null +++ b/src/app/categoryBrowse/categoryBrowse.js @@ -0,0 +1,75 @@ +angular.module('orderCloud') + .config(CategoryBrowseConfig) + .controller('CategoryBrowseCtrl', CategoryBrowseController) +; + +function CategoryBrowseConfig($stateProvider, catalogid){ + $stateProvider + .state('categoryBrowse', { + parent:'base', + url:'/browse/categories?categoryID?productPage?categoryPage?pageSize?sortBy?filters', + templateUrl:'categoryBrowse/templates/categoryBrowse.tpl.html', + controller:'CategoryBrowseCtrl', + controllerAs:'categoryBrowse', + resolve: { + Parameters: function($stateParams, ocParameters) { + return ocParameters.Get($stateParams); + }, + CategoryList: function(OrderCloud, Parameters) { + if(Parameters.categoryID) { Parameters.filters ? Parameters.filters.ParentID = Parameters.categoryID : Parameters.filters = {ParentID:Parameters.categoryID}; } + return OrderCloud.Me.ListCategories(null, Parameters.categoryPage, Parameters.pageSize || 12, null, Parameters.sortBy, Parameters.filters, 1); + }, + ProductList: function(OrderCloud, Parameters) { + if(Parameters && Parameters.filters && Parameters.filters.ParentID) { + delete Parameters.filters.ParentID; + return OrderCloud.Me.ListProducts(null, Parameters.productPage, Parameters.pageSize || 12, null, Parameters.sortBy, Parameters.filters, Parameters.categoryID); + } else { + return null; + } + }, + SelectedCategory: function(OrderCloud, Parameters){ + if(Parameters.categoryID){ + return OrderCloud.Me.ListCategories(null, 1, 1, null, null, {ID:Parameters.categoryID}, 'all') + .then(function(data){ + return data.Items[0]; + }); + + } else { + return null; + } + + } + } + }); +} + +function CategoryBrowseController($state, ocParameters, CategoryList, ProductList, Parameters, SelectedCategory) { + var vm = this; + vm.categoryList = CategoryList; + vm.productList = ProductList; + vm.parameters = Parameters; + vm.selectedCategory = SelectedCategory; + + vm.getNumberOfResults = function(list){ + return vm[list].Meta.ItemRange[0] + ' - ' + vm[list].Meta.ItemRange[1] + ' of ' + vm[list].Meta.TotalCount + ' results'; + }; + + vm.filter = function(resetPage) { + $state.go('.', ocParameters.Create(vm.parameters, resetPage)); + }; + + vm.updateCategoryList = function(category){ + vm.parameters.categoryID = category; + vm.filter(true); + }; + + vm.changeCategoryPage = function(newPage){ + vm.parameters.categoryPage = newPage; + vm.filter(false); + }; + + vm.changeProductPage = function(newPage){ + vm.parameters.productPage = newPage; + vm.filter(false); + }; +} diff --git a/src/app/categoryBrowse/templates/categoryBrowse.tpl.html b/src/app/categoryBrowse/templates/categoryBrowse.tpl.html new file mode 100644 index 00000000..5ff76499 --- /dev/null +++ b/src/app/categoryBrowse/templates/categoryBrowse.tpl.html @@ -0,0 +1,56 @@ +
+ +
+ + {{categoryBrowse.getNumberOfResults('categoryList')}}
+
+
+
+ +
+

{{category.Name || category.ID}}

+

{{category.ID}}

+
+
+
+
+
+ + + + +
+ + {{categoryBrowse.getNumberOfResults('productList')}}
+
+
+
+
+
+ + + + +
+ +
+ No products or categories +
+
+
\ No newline at end of file diff --git a/src/app/categoryBrowse/tests/categoryBrowse.spec.js b/src/app/categoryBrowse/tests/categoryBrowse.spec.js new file mode 100644 index 00000000..fa6fa214 --- /dev/null +++ b/src/app/categoryBrowse/tests/categoryBrowse.spec.js @@ -0,0 +1,114 @@ +describe('Component: Category Browse', function(){ + var scope, + q, + oc, + state, + _ocParameters, + mockProductList, + categoryList + ; + beforeEach(module(function($provide) { + $provide.value('Parameters', {categoryPage: null, productPage: null, pageSize: null, sortBy: null, filters: null, categoryID:null}); + })); + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($rootScope, $q, OrderCloud, ocParameters, $state){ + scope = $rootScope.$new(); + q = $q; + oc = OrderCloud; + state = $state; + _ocParameters = ocParameters; + categoryList = ['category1', 'category2']; + mockProductList = { + Items:['product1', 'product2'], + Meta:{ + ItemRange:[1, 3], + TotalCount: 50 + } + }; + })); + + describe('State: categoryBrowse', function(){ + var state; + beforeEach(inject(function($state){ + state = $state.get('categoryBrowse'); + var defer = q.defer(); + defer.resolve(); + spyOn(_ocParameters, 'Get'); + spyOn(oc.Me, 'ListCategories'); + spyOn(oc.Me, 'ListProducts'); + })); + it('should resolve Parameters', inject(function($injector){ + $injector.invoke(state.resolve.Parameters); + expect(_ocParameters.Get).toHaveBeenCalled(); + })); + it('should resolve CategoryList', inject(function($injector){ + $injector.invoke(state.resolve.CategoryList); + expect(oc.Me.ListCategories).toHaveBeenCalled(); + })); + it('CategoryList resolve should return subcategories of categoryID', inject(function($injector, Parameters){ + Parameters.categoryID = '12'; + $injector.invoke(state.resolve.CategoryList); + expect(oc.Me.ListCategories).toHaveBeenCalledWith(null, null, 12, null, null, {ParentID:'12'}, 1); + })); + it('should resolve ProductList', inject(function($injector, Parameters){ + Parameters.filters = {ParentID:'12'}; + $injector.invoke(state.resolve.ProductList); + expect(oc.Me.ListProducts).toHaveBeenCalled(); + })); + it('ProductList should not return products when there is no ParentID filter', inject(function($injector){ + //we don't want to return products on the top category level + $injector.invoke(state.resolve.ProductList); + expect(oc.Me.ListProducts).not.toHaveBeenCalled(); + })); + }); + + describe('Controller: CategoryBrowseController', function(){ + var categoryBrowseCtrl; + beforeEach(inject(function($state, $controller, Parameters){ + var state = $state; + var selectedCategory = 'category1'; + categoryBrowseCtrl = $controller('CategoryBrowseCtrl', { + $scope: scope, + $state: state, + ocParameters: _ocParameters, + CategoryList: categoryList, + ProductList: mockProductList, + Parameters: Parameters, + SelectedCategory: selectedCategory + }); + spyOn(state, 'go'); + spyOn(_ocParameters, 'Create'); + })); + describe('filter', function(){ + it('should reload state and call ocParameters.Create with any parameters', function(){ + categoryBrowseCtrl.parameters = {pageSize: 1}; + categoryBrowseCtrl.filter(true); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith({pageSize:1}, true); + }); + }); + describe('updateCategoryList', function(){ + it('should reload state with new category ID parameter', function(){ + categoryBrowseCtrl.updateCategoryList('newCategoryID'); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith({categoryPage: null, productPage: null, pageSize: null, sortBy: null, filters: null, categoryID:'newCategoryID'}, true); + }); + }); + describe('changeCategoryPage', function(){ + it('should reload state with the new categoryPage', function(){ + categoryBrowseCtrl.changeCategoryPage('newCategoryPage'); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith({categoryPage: 'newCategoryPage', productPage: null, pageSize: null, sortBy: null, filters: null, categoryID: null}, false); + }); + }); + describe('changeProductPage', function(){ + it('should reload state with the new productPage', function(){ + categoryBrowseCtrl.changeProductPage('newProductPage', function(){ + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith({categoryPage: null, productPage: 'newProductPage', pageSize: null, sortBy: null, filters: null, categoryID: null}, false); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/checkout/checkout.js b/src/app/checkout/checkout.js new file mode 100644 index 00000000..f9f9988c --- /dev/null +++ b/src/app/checkout/checkout.js @@ -0,0 +1,162 @@ +angular.module('orderCloud') + .config(checkoutConfig) + .controller('CheckoutCtrl', CheckoutController) + .factory('AddressSelectModal', AddressSelectModalService) + .controller('AddressSelectCtrl', AddressSelectController) + .constant('CheckoutConfig', { + ShippingRates: true, + TaxRates: false, + AvailablePaymentMethods: ['PurchaseOrder', 'CreditCard', 'SpendingAccount'] + }) +; + +function checkoutConfig($urlRouterProvider, $stateProvider) { + $urlRouterProvider.when('/checkout', '/checkout/shipping'); + $stateProvider + .state('checkout', { + abstract:true, + parent: 'base', + url: '/checkout', + templateUrl: 'checkout/templates/checkout.tpl.html', + controller: 'CheckoutCtrl', + controllerAs: 'checkout', + resolve: { + OrderShipAddress: function($q, OrderCloud, CurrentOrder){ + var deferred = $q.defer(); + if (CurrentOrder.ShippingAddressID) { + OrderCloud.Me.GetAddress(CurrentOrder.ShippingAddressID) + .then(function(address) { + deferred.resolve(address); + }) + .catch(function(ex) { + deferred.resolve(null); + }); + } + else { + deferred.resolve(null); + } + + return deferred.promise; + }, + CurrentPromotions: function(CurrentOrder, OrderCloud) { + return OrderCloud.Orders.ListPromotions(CurrentOrder.ID); + }, + OrderBillingAddress: function($q, OrderCloud, CurrentOrder){ + var deferred = $q.defer(); + + if (CurrentOrder.BillingAddressID) { + OrderCloud.Me.GetAddress(CurrentOrder.BillingAddressID) + .then(function(address) { + deferred.resolve(address); + }) + .catch(function(ex) { + deferred.resolve(null); + }); + } + else { + deferred.resolve(null); + } + return deferred.promise; + } + } + }) + ; +} + +function CheckoutController($state, $rootScope, toastr, OrderCloud, OrderShipAddress, CurrentPromotions, OrderBillingAddress, CheckoutConfig) { + var vm = this; + vm.shippingAddress = OrderShipAddress; + vm.billingAddress = OrderBillingAddress; + vm.promotions = CurrentPromotions.Items; + vm.checkoutConfig = CheckoutConfig; + + vm.submitOrder = function(order) { + OrderCloud.Orders.Submit(order.ID) + .then(function(order) { + $state.go('confirmation', {orderid:order.ID}, {reload:'base'}); + toastr.success('Your order has been submitted', 'Success'); + }) + .catch(function(ex) { + toastr.error('Your order did not submit successfully.', 'Error'); + }); + }; + + $rootScope.$on('OC:OrderShipAddressUpdated', function(event, order) { + OrderCloud.Me.GetAddress(order.ShippingAddressID) + .then(function(address){ + vm.shippingAddress = address; + }); + }); + + $rootScope.$on('OC:OrderBillAddressUpdated', function(event, order){ + OrderCloud.Me.GetAddress(order.BillingAddressID) + .then(function(address){ + vm.billingAddress = address; + }); + }); + + vm.removePromotion = function(order, promotion) { + OrderCloud.Orders.RemovePromotion(order.ID, promotion.Code) + .then(function() { + $rootScope.$broadcast('OC:UpdatePromotions', order.ID); + }) + }; + + $rootScope.$on('OC:UpdatePromotions', function(event, orderid) { + OrderCloud.Orders.ListPromotions(orderid) + .then(function(data) { + if (data.Meta) { + vm.promotions = data.Items; + } else { + vm.promotions = data; + } + $rootScope.$broadcast('OC:UpdateOrder', orderid); + }) + }); +} + +function AddressSelectModalService($uibModal) { + var service = { + Open: _open + }; + + function _open(type) { + return $uibModal.open({ + templateUrl: 'checkout/templates/addressSelect.modal.tpl.html', + controller: 'AddressSelectCtrl', + controllerAs: 'addressSelect', + backdrop: 'static', + size: 'md', + resolve: { + Addresses: function(OrderCloud) { + if (type == 'shipping') { + return OrderCloud.Me.ListAddresses(null, 1, 100, null, null, {Shipping: true}); + } else if (type == 'billing') { + return OrderCloud.Me.ListAddresses(null, 1, 100, null, null, {Billing: true}); + } else { + return OrderCloud.Me.ListAddresses(null, 1, 100); + } + } + } + }).result; + } + + return service; +} + +function AddressSelectController($uibModalInstance, Addresses) { + var vm = this; + vm.addresses = Addresses; + + vm.select = function (address) { + $uibModalInstance.close(address); + }; + + vm.createAddress = function() { + $uibModalInstance.close('create'); + }; + + vm.cancel = function () { + $uibModalInstance.dismiss(); + }; +} \ No newline at end of file diff --git a/src/app/checkout/checkout.md b/src/app/checkout/checkout.md new file mode 100644 index 00000000..885c705d --- /dev/null +++ b/src/app/checkout/checkout.md @@ -0,0 +1,7 @@ +## Checkout Component Overview + +This component allows buyer users with an open order to submit their order with shipping, billing, and payment information. + +It uses the Orders, and LineItems resources to build up the data. + +Checkout is a buyer perspective component, and can only be accessed when logged in as a buyer user, or when impersonating a buyer user. diff --git a/src/app/checkout/confirmation/checkout.confirmation.js b/src/app/checkout/confirmation/checkout.confirmation.js new file mode 100644 index 00000000..4c056695 --- /dev/null +++ b/src/app/checkout/confirmation/checkout.confirmation.js @@ -0,0 +1,85 @@ +angular.module('orderCloud') + .config(checkoutConfirmationConfig) + .controller('CheckoutConfirmationCtrl', CheckoutConfirmationController); + +function checkoutConfirmationConfig($stateProvider) { + $stateProvider + .state('confirmation', { + parent: 'base', + url: '/confirmation/:orderid', + templateUrl: 'checkout/confirmation/templates/checkout.confirmation.tpl.html', + controller: 'CheckoutConfirmationCtrl', + controllerAs: 'checkoutConfirmation', + resolve: { + SubmittedOrder: function($stateParams, OrderCloud) { + return OrderCloud.Me.GetOrder($stateParams.orderid); + }, + OrderShipAddress: function(SubmittedOrder, OrderCloud){ + return OrderCloud.Me.GetAddress(SubmittedOrder.ShippingAddressID); + }, + OrderPromotions: function(SubmittedOrder, OrderCloud) { + return OrderCloud.Orders.ListPromotions(SubmittedOrder.ID); + }, + OrderBillingAddress: function(SubmittedOrder, OrderCloud){ + return OrderCloud.Me.GetAddress(SubmittedOrder.BillingAddressID); + }, + OrderPayments: function($q, SubmittedOrder, OrderCloud) { + var deferred = $q.defer(); + OrderCloud.Payments.List(SubmittedOrder.ID) + .then(function(data) { + var queue = []; + angular.forEach(data.Items, function(payment, index) { + if (payment.Type === 'CreditCard' && payment.CreditCardID) { + queue.push((function() { + var d = $q.defer(); + OrderCloud.Me.GetCreditCard(payment.CreditCardID) + .then(function(cc) { + data.Items[index].Details = cc; + d.resolve(); + }); + return d.promise; + })()); + } else if (payment.Type === 'SpendingAccount' && payment.SpendingAccountID) { + queue.push((function() { + var d = $q.defer(); + OrderCloud.Me.GetSpendingAccount(payment.SpendingAccountID) + .then(function(cc) { + data.Items[index].Details = cc; + d.resolve(); + }); + return d.promise; + })()); + } + }); + $q.all(queue) + .then(function() { + deferred.resolve(data); + }) + }); + + return deferred.promise; + }, + LineItemsList: function($q, $state, toastr, ocLineItems, SubmittedOrder, OrderCloud) { + var dfd = $q.defer(); + OrderCloud.LineItems.List(SubmittedOrder.ID) + .then(function(data) { + ocLineItems.GetProductInfo(data.Items) + .then(function() { + dfd.resolve(data); + }); + }); + return dfd.promise; + } + } + }); +} + +function CheckoutConfirmationController(SubmittedOrder, OrderShipAddress, OrderPromotions, OrderBillingAddress, OrderPayments, LineItemsList) { + var vm = this; + vm.order = SubmittedOrder; + vm.shippingAddress = OrderShipAddress; + vm.promotions = OrderPromotions.Items; + vm.billingAddress = OrderBillingAddress; + vm.payments = OrderPayments.Items; + vm.lineItems = LineItemsList; +} \ No newline at end of file diff --git a/src/app/checkout/confirmation/templates/checkout.confirmation.tpl.html b/src/app/checkout/confirmation/templates/checkout.confirmation.tpl.html new file mode 100644 index 00000000..da1befe8 --- /dev/null +++ b/src/app/checkout/confirmation/templates/checkout.confirmation.tpl.html @@ -0,0 +1,138 @@ +
+ +
+
+
Order ID: {{checkoutConfirmation.order.ID}}
+
Date Submitted: {{checkoutConfirmation.order.DateSubmitted | date:'short'}}
+
Subtotal: {{checkoutConfirmation.order.Subtotal | currency}}
+
Shipping: + {{checkoutConfirmation.order.ShippingCost | currency}}
+
+ {{promotion.Code}} + - {{promotion.Amount | currency}}
+
+

Total: {{checkoutConfirmation.order.Total | currency}}

+
+

+ +

+
+
+

{{payment.Type | humanize}} {{payment.Amount | currency}}

+ +

PO#: {{payment.xp.PONumber}}

+ +

+ + XXXX-XXXX-XXXX-{{payment.Details.PartialAccountNumber}} +

+ +

+ {{payment.Details.Name}}
+ Remaining Balance: {{payment.Details.Balance | currency}} +

+
+
+
+
+
+
+
+
Delivery Address
+
+
+

+
+
+
+
+
Billing Address
+
+
+

+
+
+
+
+
+
+
+

+ +

+
+ +
+
+
+ {{lineItem.Product.xp.image.Name || 'Product Image'}} +
+
+
+
+
+

+ {{lineItem.Product.Name}} +

+ {{lineItem.ProductID}} +
    +
  • + {{spec.Name}}: + {{spec.Value}} +
  • +
+
+
+
+
+

{{lineItem.UnitPrice | currency}}

+
+
+

+ {{lineItem.Quantity}} +

+ + {{'x ' + lineItem.Product.QuantityMultiplier + (lineItem.Quantity ? (' (' + (lineItem.Quantity * lineItem.Product.QuantityMultiplier) + ')') : '')}} + +
+
+

{{lineItem.LineTotal | currency}}

+
+
+
+
+
+
+
+
+
+ +
diff --git a/src/app/checkout/payment/checkout.payment.js b/src/app/checkout/payment/checkout.payment.js new file mode 100644 index 00000000..e7ea2ba0 --- /dev/null +++ b/src/app/checkout/payment/checkout.payment.js @@ -0,0 +1,88 @@ +angular.module('orderCloud') + .config(checkoutPaymentConfig) + .controller('CheckoutPaymentCtrl', CheckoutPaymentController) + .factory('CheckoutPaymentService', CheckoutPaymentService) +; + +function checkoutPaymentConfig($stateProvider) { + $stateProvider + .state('checkout.payment', { + url: '/payment', + templateUrl: 'checkout/payment/templates/checkout.payment.tpl.html', + controller: 'CheckoutPaymentCtrl', + controllerAs: 'checkoutPayment' + }) + ; +} + +function CheckoutPaymentController($exceptionHandler, $rootScope, toastr, OrderCloud, AddressSelectModal, MyAddressesModal) { + var vm = this; + vm.createAddress = createAddress; + vm.changeBillingAddress = changeBillingAddress; + + function createAddress(order){ + return MyAddressesModal.Create() + .then(function(address) { + toastr.success('Address Created', 'Success'); + order.BillingAddressID = address.ID; + saveBillingAddress(order); + }); + } + + function changeBillingAddress(order) { + AddressSelectModal.Open('billing') + .then(function(address) { + if (address == 'create') { + createAddress(order); + } else { + order.BillingAddressID = address.ID; + saveBillingAddress(order); + } + }); + } + + function saveBillingAddress(order) { + if (order && order.BillingAddressID) { + OrderCloud.Orders.Patch(order.ID, {BillingAddressID: order.BillingAddressID}) + .then(function(updatedOrder) { + $rootScope.$broadcast('OC:OrderBillAddressUpdated', updatedOrder); + }) + .catch(function(ex) { + $exceptionHandler(ex); + }); + } + } +} + +function CheckoutPaymentService($q, OrderCloud) { + var service = { + PaymentsExceedTotal: _paymentsExceedTotal, + RemoveAllPayments: _removeAllPayments + }; + + function _paymentsExceedTotal(payments, orderTotal) { + var paymentTotal = 0; + angular.forEach(payments.Items, function(payment) { + paymentTotal += payment.Amount; + }); + + return paymentTotal.toFixed(2) > orderTotal; + } + + function _removeAllPayments(payments, order) { + var deferred = $q.defer(); + + var queue = []; + angular.forEach(payments.Items, function(payment) { + queue.push(OrderCloud.Payments.Delete(order.ID, payment.ID)); + }); + + $q.all(queue).then(function() { + deferred.resolve(); + }); + + return deferred.promise; + } + + return service; +} diff --git a/src/app/checkout/payment/directives/README.md b/src/app/checkout/payment/directives/README.md new file mode 100644 index 00000000..0e316ca8 --- /dev/null +++ b/src/app/checkout/payment/directives/README.md @@ -0,0 +1,28 @@ +#Payment Directives + +**All 5 directives accept a global `order` option for passing in `base.currentOrder`** + +**oc-payment-po** => single payment, single type of purchase order +- _options:_ + - `payment`: the payment to be tied to the directive functionality (for multiple payments) + +**oc-payment-sa** => single payment, single type of spending account +- _options:_ + - `excluded-options`: an array of spending account IDs to be excluded + - `payment`: the payment to be tied to the directive functionality (for multiple payments) + +**oc-payment-cc** => single payment, single type of credit card +- _options:_ + - `excluded-options`: an array of credit card IDs to be excluded + - `payment`: the payment to be tied to the directive functionality (for multiple payments) + +**oc-payment** => single payment, multiple types (configurable) +- _options:_ + - `excluded-options`: an object with a property for `SpendingAccounts` & `CreditCards` which are an array of IDs to be excluded + - `payment-index`: the $index of the payment when using multiple payments, payment + - `payment`: the payment to be tied to the directive functionality (for multiple payments) + - `methods`: an array of methods to be made available in the payment method dropdown + +**oc-payments** => multiple payments, multiple types (configurable) +- _options_ + - `methods`: an array of methods to be made available in the payment method dropdown \ No newline at end of file diff --git a/src/app/checkout/payment/directives/payment.directives.js b/src/app/checkout/payment/directives/payment.directives.js new file mode 100644 index 00000000..aac75bcf --- /dev/null +++ b/src/app/checkout/payment/directives/payment.directives.js @@ -0,0 +1,377 @@ +angular.module('orderCloud') + + //Single Purchase Order Payment + .directive('ocPaymentPo', OCPaymentPurchaseOrder) + .controller('PaymentPurchaseOrderCtrl', PaymentPurchaseOrderController) + + //Single Spending Account Payment + .directive('ocPaymentSa', OCPaymentSpendingAccount) + .controller('PaymentSpendingAccountCtrl', PaymentSpendingAccountController) + + //Single Credit Card Payment + .directive('ocPaymentCc', OCPaymentCreditCard) + .controller('PaymentCreditCardCtrl', PaymentCreditCardController) + + //Single Payment, Multiple Types + .directive('ocPayment', OCPayment) + .controller('PaymentCtrl', PaymentController) + + //Multiple Payment, Multiple Types + .directive('ocPayments', OCPayments) + .controller('PaymentsCtrl', PaymentsController) +; + + +function OCPaymentPurchaseOrder() { + return { + restrict:'E', + scope: { + order: '=', + payment: '=?' + }, + templateUrl: 'checkout/payment/directives/templates/purchaseOrder.tpl.html', + controller: 'PaymentPurchaseOrderCtrl' + } +} + +function PaymentPurchaseOrderController($scope, $rootScope, toastr, OrderCloud, $exceptionHandler) { + if (!$scope.payment) { + OrderCloud.Payments.List($scope.order.ID) + .then(function(data) { + if (data.Items.length) { + OrderCloud.Payments.Patch($scope.order.ID, data.Items[0].ID, { + Type: 'PurchaseOrder', + CreditCardID: null, + SpendingAccountID: null, + Amount: null + }).then(function(data) { + $scope.payment = data; + }); + } else { + OrderCloud.Payments.Create($scope.order.ID, {Type: 'PurchaseOrder'}) + .then(function(data) { + $scope.payment = data; + }); + } + }); + } else if (!($scope.payment.Type == "PurchaseOrder" && $scope.payment.CreditCardID == null && $scope.payment.SpendingAccountID == null)) { + $scope.payment.Type = "PurchaseOrder"; + $scope.payment.CreditCardID = null; + $scope.payment.SpendingAccountID = null; + OrderCloud.Payments.Patch($scope.order.ID, $scope.payment.ID, $scope.payment).then(function() { + toastr.success('Paying by purchase order', 'Purchase Order Payment'); + $rootScope.$broadcast('OC:PaymentsUpdated'); + }); + } + + $scope.updatePayment = function() { + if ($scope.payment.xp && $scope.payment.xp.PONumber && (!$scope.payment.xp.PONumber.length)) $scope.payment.xp.PONumber = null; + OrderCloud.Payments.Update($scope.order.ID, $scope.payment.ID, $scope.payment) + .then(function() { + toastr.success('Purchase Order Number Saved'); + $rootScope.$broadcast('OC:PaymentsUpdated'); + }) + .catch(function(ex) { + $exceptionHandler(ex); + }); + } +} + +function OCPaymentSpendingAccount() { + return { + restrict:'E', + scope: { + order: '=', + payment: '=?', + excludedSpendingAccounts: '=?excludeOptions' + }, + templateUrl: 'checkout/payment/directives/templates/spendingAccount.tpl.html', + controller: 'PaymentSpendingAccountCtrl', + controllerAs: 'paymentSA' + } +} + +function PaymentSpendingAccountController($scope, $rootScope, toastr, OrderCloud, $exceptionHandler) { + OrderCloud.Me.ListSpendingAccounts(null, 1, 100, null, null, {RedemptionCode: '!*', AllowAsPaymentMethod: true}) + .then(function(data) { + $scope.spendingAccounts = data.Items; + }); + + if (!$scope.payment) { + OrderCloud.Payments.List($scope.order.ID) + .then(function(data) { + if (data.Items.length) { + OrderCloud.Payments.Patch($scope.order.ID, data.Items[0].ID, { + Type: 'SpendingAccount', + xp: { + PONumber:null + }, + CreditCardID:null, + SpendingAccountID:null, + Amount:null + }).then(function(data) { + $scope.payment = data; + if (!$scope.payment.SpendingAccountID) $scope.showPaymentOptions = true; + }); + } else { + OrderCloud.Payments.Create($scope.order.ID, {Type: 'SpendingAccount'}) + .then(function(data) { + $scope.payment = data; + $scope.showPaymentOptions = true; + }); + } + }); + } else { + delete $scope.payment.CreditCardID; + if ($scope.payment.xp && $scope.payment.xp.PONumber) $scope.payment.xp.PONumber = null; + if (!$scope.payment.SpendingAccountID) $scope.showPaymentOptions = true; + } + + $scope.changePayment = function() { + $scope.showPaymentOptions = true; + }; + + $scope.updatePayment = function(scope) { + var oldSelection = angular.copy($scope.payment.SpendingAccountID); + $scope.payment.SpendingAccountID = scope.spendingAccount.ID; + $scope.updatingSpendingAccountPayment = OrderCloud.Payments.Update($scope.order.ID, $scope.payment.ID, $scope.payment) + .then(function() { + $scope.showPaymentOptions = false; + toastr.success('Using ' + scope.spendingAccount.Name,'Spending Account Payment'); + $rootScope.$broadcast('OC:PaymentsUpdated'); + }) + .catch(function(ex) { + $scope.payment.SpendingAccountID = oldSelection; + $exceptionHandler(ex); + }); + }; + + $scope.$watch('payment', function(n,o) { + if (n && !n.SpendingAccountID) { + $scope.OCPaymentSpendingAccount.$setValidity('SpendingAccount_Not_Set', false); + } else { + $scope.OCPaymentSpendingAccount.$setValidity('SpendingAccount_Not_Set', true); + } + }, true); +} + +function OCPaymentCreditCard() { + return { + restrict:'E', + scope: { + order: '=', + payment: '=?', + excludedCreditCards: '=?excludeOptions' + }, + templateUrl: 'checkout/payment/directives/templates/creditCard.tpl.html', + controller: 'PaymentCreditCardCtrl', + controllerAs: 'paymentCC' + } +} + +function PaymentCreditCardController($scope, $rootScope, toastr, $filter, OrderCloud, MyPaymentCreditCardModal, $exceptionHandler) { + OrderCloud.Me.ListCreditCards(null, 1, 100, null, null, {}) + .then(function(data) { + $scope.creditCards = data.Items; + }); + + if (!$scope.payment) { + OrderCloud.Payments.List($scope.order.ID) + .then(function(data) { + if (data.Items.length) { + OrderCloud.Payments.Patch($scope.order.ID, data.Items[0].ID, { + Type: 'CreditCard', + xp: { + PONumber: null + }, + SpendingAccountID: null, + Amount: null + }).then(function(data) { + $scope.payment = data; + if (!$scope.payment.SpendingAccountID) $scope.showPaymentOptions = true; + }); + } else { + OrderCloud.Payments.Create($scope.order.ID, {Type: 'CreditCard'}) + .then(function(data) { + $scope.payment = data; + $scope.showPaymentOptions = true; + }); + } + }); + } else { + delete $scope.payment.SpendingAccountID; + if ($scope.payment.xp && $scope.payment.xp.PONumber) $scope.payment.xp.PONumber = null; + if (!$scope.payment.CreditCardID) $scope.showPaymentOptions = true; + } + + $scope.changePayment = function() { + $scope.showPaymentOptions = true; + }; + + $scope.$watch('payment', function(n,o) { + if (n && !n.CreditCardID) { + $scope.OCPaymentCreditCard.$setValidity('CreditCard_Not_Set', false); + } else { + $scope.OCPaymentCreditCard.$setValidity('CreditCard_Not_Set', true); + + } + }, true); + + $scope.updatePayment = function(scope) { + var oldSelection = angular.copy($scope.payment.CreditCardID); + $scope.payment.CreditCardID = scope.creditCard.ID; + $scope.updatingCreditCardPayment = OrderCloud.Payments.Update($scope.order.ID, $scope.payment.ID, $scope.payment) + .then(function() { + $scope.showPaymentOptions = false; + toastr.success('Using ' + $filter('humanize')(scope.creditCard.CardType) + ' ending in ' + scope.creditCard.PartialAccountNumber,'Credit Card Payment'); + $rootScope.$broadcast('OC:PaymentsUpdated'); + }) + .catch(function(ex) { + $scope.payment.CreditCardID = oldSelection; + $exceptionHandler(ex); + }); + }; + + $scope.createCreditCard = function() { + MyPaymentCreditCardModal.Create() + .then(function(card) { + toastr.success('Credit Card Created', 'Success'); + $scope.creditCards.push(card); + $scope.updatePayment({creditCard:card}); + }); + }; +} + +function OCPayment() { + return { + restrict:'E', + scope: { + order: '=', + methods: '=?', + payment: '=?', + paymentIndex: '=?', + excludeOptions: '=?' + }, + templateUrl: 'checkout/payment/directives/templates/payment.tpl.html', + controller: 'PaymentCtrl', + controllerAs: 'ocPayment' + } +} + +function PaymentController($scope, $rootScope, OrderCloud, CheckoutConfig) { + if (!$scope.methods) $scope.methods = CheckoutConfig.AvailablePaymentMethods; + if (!$scope.payment) { + OrderCloud.Payments.List($scope.order.ID) + .then(function(data) { + if (CheckoutPaymentService.PaymentsExceedTotal(data, $scope.order.Total)) { + CheckoutPaymentService.RemoveAllPayments(data, $scope.order) + .then(function(data) { + OrderCloud.Payments.Create($scope.order.ID, {Type: CheckoutConfig.AvailablePaymentMethods[0]}) + .then(function(data) { + $scope.payment = data; + $rootScope.$broadcast('OC:PaymentsUpdated'); + }); + }); + } + else if (data.Items.length) { + $scope.payment = data.Items[0]; + if ($scope.methods.length == 1) $scope.payment.Type = $scope.methods[0]; + } else { + OrderCloud.Payments.Create($scope.order.ID, {Type: CheckoutConfig.AvailablePaymentMethods[0]}) + .then(function(data) { + $scope.payment = data; + $rootScope.$broadcast('OC:PaymentsUpdated'); + }); + } + }); + } else if ($scope.methods.length == 1) { + $scope.payment.Type = $scope.methods[0]; + } +} + +function OCPayments() { + return { + restrict:'E', + scope: { + order: '=', + methods: '=?' + }, + templateUrl: 'checkout/payment/directives/templates/payments.tpl.html', + controller: 'PaymentsCtrl' + } +} + +function PaymentsController($rootScope, $scope, $exceptionHandler, toastr, OrderCloud, CheckoutPaymentService, CheckoutConfig) { + if (!$scope.methods) $scope.methods = CheckoutConfig.AvailablePaymentMethods; + + OrderCloud.Payments.List($scope.order.ID) + .then(function(data) { + if (!data.Items.length) { + $scope.payments = {Items: []}; + $scope.addNewPayment(); + } + else if (CheckoutPaymentService.PaymentsExceedTotal(data, $scope.order.Total)) { + CheckoutPaymentService.RemoveAllPayments(data, $scope.order) + .then(function(data) { + $scope.payments = {Items: []}; + $scope.addNewPayment(); + }); + } + else { + $scope.payments = data; + calculateMaxTotal(); + } + }); + + $scope.addNewPayment = function() { + OrderCloud.Payments.Create($scope.order.ID, {Type: CheckoutConfig.AvailablePaymentMethods[0]}) + .then(function(data) { + $scope.payments.Items.push(data); + calculateMaxTotal(); + toastr.success('Payment Added'); + }); + }; + + $scope.removePayment = function(scope) { + OrderCloud.Payments.Delete($scope.order.ID, scope.payment.ID) + .then(function() { + $scope.payments.Items.splice(scope.$index, 1); + calculateMaxTotal(); + toastr.success('Payment Removed'); + }); + }; + + $scope.updatePaymentAmount = function(scope) { + if (scope.payment.Amount > scope.payment.MaxAmount || !scope.payment.Amount) return; + OrderCloud.Payments.Update($scope.order.ID, scope.payment.ID, scope.payment) + .then(function(data) { + toastr.success('Payment Amount Updated'); + calculateMaxTotal(); + }) + .catch(function(ex) { + $exceptionHandler(ex); + }); + }; + + $rootScope.$on('OC:PaymentsUpdated', function() { + calculateMaxTotal(); + }); + + + function calculateMaxTotal() { + var paymentTotal = 0; + $scope.excludeOptions = { + SpendingAccounts: [], + CreditCards: [] + }; + angular.forEach($scope.payments.Items, function(payment) { + paymentTotal += payment.Amount; + if (payment.SpendingAccountID) $scope.excludeOptions.SpendingAccounts.push(payment.SpendingAccountID); + if (payment.CreditCardID) $scope.excludeOptions.CreditCards.push(payment.CreditCardID); + var maxAmount = $scope.order.Total - _.reduce(_.pluck($scope.payments.Items, 'Amount'), function(a, b) {return a + b; }); + payment.MaxAmount = (payment.Amount + maxAmount).toFixed(2); + }); + $scope.canAddPayment = paymentTotal < $scope.order.Total; + if($scope.OCPayments) $scope.OCPayments.$setValidity('Insufficient_Payment', !$scope.canAddPayment); + } +} \ No newline at end of file diff --git a/src/app/checkout/payment/directives/templates/creditCard.tpl.html b/src/app/checkout/payment/directives/templates/creditCard.tpl.html new file mode 100644 index 00000000..2153c235 --- /dev/null +++ b/src/app/checkout/payment/directives/templates/creditCard.tpl.html @@ -0,0 +1,23 @@ + +
+ + Create Credit Card +
+
+
+
+ +
+
+ {{creditCard.CardholderName}}
+ {{'XXXX-XXXX-XXXX-' + creditCard.PartialAccountNumber}}
+ Expires On: {{creditCard.ExpirationDate | date:'MM/yy'}} +
+
+ Change + Select +
+
+
+
+
\ No newline at end of file diff --git a/src/app/checkout/payment/directives/templates/payment.tpl.html b/src/app/checkout/payment/directives/templates/payment.tpl.html new file mode 100644 index 00000000..83e9f992 --- /dev/null +++ b/src/app/checkout/payment/directives/templates/payment.tpl.html @@ -0,0 +1,16 @@ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/src/app/checkout/payment/directives/templates/payments.tpl.html b/src/app/checkout/payment/directives/templates/payments.tpl.html new file mode 100644 index 00000000..0e3ee8b6 --- /dev/null +++ b/src/app/checkout/payment/directives/templates/payments.tpl.html @@ -0,0 +1,22 @@ + +
+
+
+
+
+ + +
+ +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/src/app/checkout/payment/directives/templates/purchaseOrder.tpl.html b/src/app/checkout/payment/directives/templates/purchaseOrder.tpl.html new file mode 100644 index 00000000..ede0b44d --- /dev/null +++ b/src/app/checkout/payment/directives/templates/purchaseOrder.tpl.html @@ -0,0 +1,7 @@ + +
+
+ + +
+
\ No newline at end of file diff --git a/src/app/checkout/payment/directives/templates/spendingAccount.tpl.html b/src/app/checkout/payment/directives/templates/spendingAccount.tpl.html new file mode 100644 index 00000000..a09ba30b --- /dev/null +++ b/src/app/checkout/payment/directives/templates/spendingAccount.tpl.html @@ -0,0 +1,31 @@ + +
+
+
+
+
+ {{spendingAccount.Name}} +
+ + Lifetime: {{spendingAccount.StartDate | date:'shortDate'}} - {{spendingAccount.EndDate | date :'shortDate'}} + + + Made Available On: {{spendingAccount.StartDate | date:'shortDate'}} + + + Expires On: {{spendingAccount.EndDate | date :'shortDate'}} + +
+
+
+ +

{{spendingAccount.Balance | currency}}

+
+
+ Change + Select +
+
+
+
+
\ No newline at end of file diff --git a/src/app/checkout/payment/templates/checkout.payment.tpl.html b/src/app/checkout/payment/templates/checkout.payment.tpl.html new file mode 100644 index 00000000..5e5105ae --- /dev/null +++ b/src/app/checkout/payment/templates/checkout.payment.tpl.html @@ -0,0 +1,52 @@ +
+
+
+
+

Payment

+ +
+
+ Create Address +

Billing Address

+
+ You currently do not have a billing address selected.
+ Select one now +
+
+
+
+ Change +
+

+
+
+
+
+
+
+

+ Order Summary +

+
+
+

+ +

+
Subtotal: {{base.currentOrder.Subtotal | currency}}
+
Estimated Shipping: + {{base.currentOrder.ShippingCost | currency}}
+
+ {{promotion.Code}} + - {{promotion.Amount | currency}}
+
+

Estimated Total: {{base.currentOrder.Total | currency}}

+
+ +
+
+
+
diff --git a/src/app/checkout/review/checkout.review.js b/src/app/checkout/review/checkout.review.js new file mode 100644 index 00000000..82a97f9e --- /dev/null +++ b/src/app/checkout/review/checkout.review.js @@ -0,0 +1,83 @@ +angular.module('orderCloud') + .config(checkoutReviewConfig) + .controller('CheckoutReviewCtrl', CheckoutReviewController); + +function checkoutReviewConfig($stateProvider) { + $stateProvider + .state('checkout.review', { + url: '/review', + templateUrl: 'checkout/review/templates/checkout.review.tpl.html', + controller: 'CheckoutReviewCtrl', + controllerAs: 'checkoutReview', + resolve: { + LineItemsList: function($q, $state, toastr, OrderCloud, ocLineItems, CurrentOrder) { + var dfd = $q.defer(); + OrderCloud.LineItems.List(CurrentOrder.ID) + .then(function(data) { + if (!data.Items.length) { + dfd.resolve(data); + } + else { + ocLineItems.GetProductInfo(data.Items) + .then(function() { + dfd.resolve(data); + }); + } + }) + .catch(function() { + toastr.error('Your order does not contain any line items.', 'Error'); + dfd.reject(); + }); + return dfd.promise; + }, + OrderPaymentsDetail: function($q, OrderCloud, CurrentOrder, $state) { + return OrderCloud.Payments.List(CurrentOrder.ID) + .then(function(data) { + //TODO: create a queue that can be resolved + var dfd = $q.defer(); + if (!data.Items.length) { + dfd.reject(); + $state.go('checkout.shipping'); + } + var queue = []; + angular.forEach(data.Items, function(payment, index) { + if (payment.Type === 'CreditCard' && payment.CreditCardID) { + queue.push((function() { + var d = $q.defer(); + OrderCloud.Me.GetCreditCard(payment.CreditCardID) + .then(function(cc) { + data.Items[index].Details = cc; + d.resolve(); + }); + return d.promise; + })()); + } + if (payment.Type === 'SpendingAccount' && payment.SpendingAccountID) { + queue.push((function() { + var d = $q.defer(); + OrderCloud.Me.GetSpendingAccount(payment.SpendingAccountID) + .then(function(sa) { + data.Items[index].Details = sa; + d.resolve(); + }); + return d.resolve(); + })()); + } + }); + $q.all(queue) + .then(function() { + dfd.resolve(data); + }); + return dfd.promise; + }) + + } + } + }); +} + +function CheckoutReviewController(LineItemsList, OrderPaymentsDetail) { + var vm = this; + vm.payments = OrderPaymentsDetail; + vm.lineItems = LineItemsList; +} \ No newline at end of file diff --git a/src/app/checkout/review/templates/checkout.review.tpl.html b/src/app/checkout/review/templates/checkout.review.tpl.html new file mode 100644 index 00000000..4324ec2e --- /dev/null +++ b/src/app/checkout/review/templates/checkout.review.tpl.html @@ -0,0 +1,133 @@ +
+
+
+
+
+

Delivery Address

+
+
+

+
+
+
+
+

Billing Address

+
+
+

+
+
+
+
+
+ +
+
+
+ {{lineItem.Product.xp.image.Name || 'Product Image'}} +
+
+
+
+
+

+ {{lineItem.Product.Name}} +

+ {{lineItem.ProductID}} +
    +
  • + {{spec.Name}}: + {{spec.Value}} +
  • +
+
+
+
+
+

{{lineItem.UnitPrice | currency}}

+
+
+

+ {{lineItem.Quantity}} +

+ + {{'x ' + lineItem.Product.QuantityMultiplier + (lineItem.Quantity ? (' (' + (lineItem.Quantity * lineItem.Product.QuantityMultiplier) + ')') : '')}} + +
+
+

{{lineItem.LineTotal | currency}}

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

+ Order Summary +

+
+
+

+ +

+
Subtotal: {{base.currentOrder.Subtotal | currency}}
+
Estimated Shipping: + {{base.currentOrder.ShippingCost | currency}}
+
+ {{promotion.Code}} + - {{promotion.Amount | currency}}
+
+

Total: {{base.currentOrder.Total | currency}}

+
+
+

{{payment.Type | humanize}} {{payment.Amount | currency}}

+ +

PO#: {{payment.xp.PONumber}}

+ +

+ + XXXX-XXXX-XXXX-{{payment.Details.PartialAccountNumber}} +

+ +

+ {{payment.Details.Name}}
+ Remaining Balance: {{payment.Details.Balance | currency}} +

+
+
+
+ +
+
+
+
diff --git a/src/app/checkout/shipping/checkout.shipping.js b/src/app/checkout/shipping/checkout.shipping.js new file mode 100644 index 00000000..9f30c47a --- /dev/null +++ b/src/app/checkout/shipping/checkout.shipping.js @@ -0,0 +1,83 @@ +angular.module('orderCloud') + .config(checkoutShippingConfig) + .controller('CheckoutShippingCtrl', CheckoutShippingController); + +function checkoutShippingConfig($stateProvider) { + $stateProvider + .state('checkout.shipping', { + url: '/shipping', + templateUrl: 'checkout/shipping/templates/checkout.shipping.tpl.html', + controller: 'CheckoutShippingCtrl', + controllerAs: 'checkoutShipping' + }); +} + +function CheckoutShippingController($exceptionHandler, $rootScope, toastr, OrderCloud, MyAddressesModal, AddressSelectModal, ShippingRates, CheckoutConfig) { + var vm = this; + vm.createAddress = createAddress; + vm.changeShippingAddress = changeShippingAddress; + vm.saveShipAddress = saveShipAddress; + vm.shipperSelected = shipperSelected; + vm.initShippingRates = initShippingRates; + vm.getShippingRates = getShippingRates; + vm.analyzeShipments = analyzeShipments; + + function createAddress(order) { + MyAddressesModal.Create() + .then(function(address) { + toastr.success('Address Created', 'Success'); + order.ShippingAddressID = address.ID; + vm.saveShipAddress(order); + }); + } + + function changeShippingAddress(order) { + AddressSelectModal.Open('shipping') + .then(function(address) { + if (address == 'create') { + vm.createAddress(order); + } else { + order.ShippingAddressID = address.ID; + vm.saveShipAddress(order); + } + }) + } + + function saveShipAddress(order) { + if (order && order.ShippingAddressID) { + OrderCloud.Orders.Patch(order.ID, {ShippingAddressID: order.ShippingAddressID}) + .then(function(updatedOrder) { + $rootScope.$broadcast('OC:OrderShipAddressUpdated', updatedOrder); + vm.getShippingRates(order); + }) + .catch(function(ex){ + $exceptionHandler(ex); + }); + } + } + + function initShippingRates(order) { + if (CheckoutConfig.ShippingRates && order.ShippingAddressID) vm.getShippingRates(order); + } + + function getShippingRates(order) { + vm.shippersAreLoading = true; + vm.shippersLoading = ShippingRates.GetRates(order) + .then(function(shipments) { + vm.shippersAreLoading = false; + vm.shippingRates = shipments; + vm.analyzeShipments(order); + }); + } + + function analyzeShipments(order) { + vm.shippingRates = ShippingRates.AnalyzeShipments(order, vm.shippingRates); + } + + function shipperSelected(order) { + ShippingRates.ManageShipments(order, vm.shippingRates) + .then(function() { + $rootScope.$broadcast('OC:UpdateOrder', order.ID); + }); + } +} \ No newline at end of file diff --git a/src/app/checkout/shipping/checkout.shipping.rates.js b/src/app/checkout/shipping/checkout.shipping.rates.js new file mode 100644 index 00000000..0dc8bb62 --- /dev/null +++ b/src/app/checkout/shipping/checkout.shipping.rates.js @@ -0,0 +1,131 @@ +angular.module('orderCloud') + .factory('ShippingRates', ShippingRatesService) +; + +function ShippingRatesService($q, $resource, OrderCloud, apiurl) { + var service = { + GetRates: _getRates, + GetLineItemRates: _getLineItemRates, + SetShippingCost: _setShippingCost, + ManageShipments: _manageShipments, + AnalyzeShipments: _analyzeShipments + }; + + var shippingRatesURL = apiurl + '/v1/integrationproxy/shippingrates'; + + function _getRates(order) { + var deferred = $q.defer(); + + var request = { + BuyerID: OrderCloud.BuyerID.Get(), + TransactionType: 'GetRates', + OrderID: order.ID + }; + + $resource(shippingRatesURL, {}, {getrates: {method: 'POST', headers: {'Authorization': 'Bearer ' + OrderCloud.Auth.ReadToken()}}}).getrates(request).$promise + .then(function(data) { + deferred.resolve(data.ResponseBody.Shipments); + }) + .catch(function(ex) { + deferred.resolve(null); + }); + + return deferred.promise; + } + + function _getLineItemRates(order) { + var deferred = $q.defer(); + + var request = { + BuyerID: OrderCloud.BuyerID.Get(), + TransactionType: 'GetLineItemRates', + OrderID: order.ID + }; + + $resource(shippingRatesURL, {}, {getlineitemrates: {method: 'POST', headers: {'Authorization': 'Bearer ' + OrderCloud.Auth.ReadToken()}}}).getlineitemrates(request).$promise + .then(function(data) { + deferred.resolve(data.ResponseBody.Shipments); + }) + .catch(function(ex) { + deferred.resolve(null); + }); + + return deferred.promise; + } + + function _setShippingCost(order, cost) { + var deferred = $q.defer(); + + var request = { + BuyerID: OrderCloud.BuyerID.Get(), + TransactionType: 'SetShippingCost', + OrderID: order.ID, + ShippingCost: cost + }; + + $resource(shippingRatesURL, {}, {setshippingcost: {method: 'POST', headers: {'Authorization': 'Bearer ' + OrderCloud.Auth.ReadToken()}}}).setshippingcost(request).$promise + .then(function(data) { + deferred.resolve(data.ResponseBody); + }) + .catch(function(ex) { + deferred.resolve(null); + }); + + return deferred.promise; + } + + function _manageShipments(order, shipments) { + var deferred = $q.defer(); + + var xpPatch = {Shippers: []}; + var shippingCost = 0; + + angular.forEach(shipments, function(shipment) { + if (shipment.SelectedShipper) { + shippingCost += shipment.SelectedShipper.Price; + xpPatch.Shippers.push({ + Shipper: shipment.SelectedShipper.Description, + Cost: shipment.SelectedShipper.Price, + LineItemIDs: shipment.LineItemIDs + }); + } + }); + + OrderCloud.Orders.Patch(order.ID, {xp: xpPatch}) + .then(function() { + updateShippingCost(); + }) + .catch(function() { + deferred.reject(); + }); + + function updateShippingCost() { + _setShippingCost(order, shippingCost) + .then(function(data) { + deferred.resolve(data); + }) + .catch(function(ex) { + deferred.reject(); + }); + } + + return deferred.promise; + } + + function _analyzeShipments(order, shippingRates) { + if (order.xp && order.xp.Shippers) { + angular.forEach(order.xp.Shippers, function(shipment) { + angular.forEach(shippingRates, function(s) { + if (_.intersection(s.LineItemIDs, shipment.LineItemIDs).length == shipment.LineItemIDs.length) { + var selection = _.findWhere(s.Rates, {Description: shipment.Shipper}); + if (selection) s.SelectedShipper = selection; + } + }); + }); + } + + return shippingRates; + } + + return service; +} diff --git a/src/app/checkout/shipping/templates/checkout.shipping.tpl.html b/src/app/checkout/shipping/templates/checkout.shipping.tpl.html new file mode 100644 index 00000000..d79915a5 --- /dev/null +++ b/src/app/checkout/shipping/templates/checkout.shipping.tpl.html @@ -0,0 +1,66 @@ +
+
+
+
+ New Address +

Delivery Address

+
+
+
+ Change +
+

+
+
+
+ You currently do not have a shipping address selected.
+ Select one now +
+
+
+

Shipping Method

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

+ Order Summary +

+
+
+

+ +

+
Subtotal: {{base.currentOrder.Subtotal | currency}}
+
Estimated Shipping: + {{base.currentOrder.ShippingCost | currency}}
+
+
+ {{promotion.Code}} + remove + - {{promotion.Amount | currency}}
+
+

Estimated Total: {{base.currentOrder.Total | currency}}

+
+ +
+
+
+
\ No newline at end of file diff --git a/src/app/checkout/templates/addressSelect.modal.tpl.html b/src/app/checkout/templates/addressSelect.modal.tpl.html new file mode 100644 index 00000000..095f5fa2 --- /dev/null +++ b/src/app/checkout/templates/addressSelect.modal.tpl.html @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/src/app/checkout/templates/checkout.tpl.html b/src/app/checkout/templates/checkout.tpl.html new file mode 100644 index 00000000..1fab3497 --- /dev/null +++ b/src/app/checkout/templates/checkout.tpl.html @@ -0,0 +1,18 @@ +
+ +
+
+
\ No newline at end of file diff --git a/src/app/checkout/tests/checkout.confirmation.spec.js b/src/app/checkout/tests/checkout.confirmation.spec.js new file mode 100644 index 00000000..2555ea12 --- /dev/null +++ b/src/app/checkout/tests/checkout.confirmation.spec.js @@ -0,0 +1,164 @@ +describe('Component: Checkout Confirmation', function() { + var scope, + q, + oc, + lineItemHelpers, + lineItemsList, + orderPayments, + order, + submittedOrder, + address; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(module(function($provide) { + $provide.value('OrderPayments', { + Items: [{Type: 'CreditCard', CreditCardID: 'CC123'}, {Type: 'SpendingAccount', SpendingAccountID: 'SA123'}], + Meta: { + Page: 1, + PageSize: 20, + TotalCount: 0, + TotalPages: 1, + ItemRange: [1, 0] + } + }); + $provide.value('SubmittedOrder', { + ID: 'SubmittedOrder123', + BillingAddressID: 'TestAddress123456789', + ShippingAddressID: 'TestAddress123456789', + BillingAddress: { + ID: 'TestAddress123456789' + } + }); + })); + beforeEach(inject(function($q, $rootScope, OrderCloud, ocLineItems, SubmittedOrder, OrderPayments) { + q = $q; + oc = OrderCloud; + scope = $rootScope.$new(); + lineItemHelpers = ocLineItems; + submittedOrder = SubmittedOrder; + orderPayments = OrderPayments; + order = { + ID: 'TestOrder123456789', + Type: 'Standard', + FromUserID: 'TestUser123456789', + BillingAddressID: 'TestAddress123456789', + BillingAddress: { + ID: 'TestAddress123456789' + }, + ShippingAddressID: 'TestAddress123456789', + Comments: null, + ShippingCost: null, + TaxCost: null, + Subtotal: 10, + Total: 10 + }; + lineItemsList = { + Items : [{ID: '1'}, {ID: '2'}], + Meta : { + Page: 1, + PageSize: 20, + TotalCount: 2, + TotalPages: 1, + ItemRange: [1,2] + } + }; + address = { + ID: 'TestAddress123456789' + }; + })); + + describe('State: confirmation', function() { + var state, stateParams; + beforeEach(inject(function($state, $stateParams) { + state = $state.get('confirmation'); + stateParams = $stateParams; + stateParams.orderid = "SubmittedOrder123"; + var submittedOrderDefer = q.defer(); + submittedOrderDefer.resolve(submittedOrder); + spyOn(oc.Me, 'GetOrder').and.returnValue(submittedOrderDefer.promise); + + var defer = q.defer(); + defer.resolve(); + spyOn(oc.Me, 'GetAddress').and.returnValue(defer.promise); + spyOn(oc.Orders, 'ListPromotions').and.returnValue(defer.promise); + + var paymentsDefer = q.defer(); + paymentsDefer.resolve(orderPayments); + spyOn(oc.Payments, 'List').and.returnValue(paymentsDefer.promise); + spyOn(oc.Me, 'GetCreditCard').and.returnValue(defer.promise); + spyOn(oc.Me, 'GetSpendingAccount').and.returnValue(defer.promise); + + var lineItemListDefer = q.defer(); + lineItemListDefer.resolve(lineItemsList); + spyOn(oc.LineItems, 'List').and.returnValue(lineItemListDefer.promise); + spyOn(lineItemHelpers, 'GetProductInfo').and.returnValue(lineItemListDefer.promise); + })); + it('should call Me.GetOrder for submitted order', inject(function($injector) { + $injector.invoke(state.resolve.SubmittedOrder); + expect(oc.Me.GetOrder).toHaveBeenCalledWith('SubmittedOrder123'); + })); + it('should call Me.GetAddress for ShippingAddressID', inject(function($injector) { + $injector.invoke(state.resolve.OrderShipAddress); + expect(oc.Me.GetAddress).toHaveBeenCalledWith(submittedOrder.ShippingAddressID); + })); + it('should call Orders.ListPromotions', inject(function($injector) { + $injector.invoke(state.resolve.OrderPromotions); + expect(oc.Orders.ListPromotions).toHaveBeenCalledWith(submittedOrder.ID); + })); + it('should call Me.GetAddress for BillingAddressID', inject(function($injector) { + $injector.invoke(state.resolve.OrderBillingAddress); + expect(oc.Me.GetAddress).toHaveBeenCalledWith(submittedOrder.BillingAddressID); + })); + it('should call Payments.List', inject(function($injector) { + $injector.invoke(state.resolve.OrderPayments); + expect(oc.Payments.List).toHaveBeenCalledWith(submittedOrder.ID); + })); + it('should call Me.GetCreditCard for first payment', inject(function($injector) { + $injector.invoke(state.resolve.OrderPayments); + scope.$digest(); + expect(oc.Me.GetCreditCard).toHaveBeenCalledWith(orderPayments.Items[0].CreditCardID); + })); + it('should call Me.GetSpendingAccount for second payment', inject(function($injector) { + $injector.invoke(state.resolve.OrderPayments); + scope.$digest(); + expect(oc.Me.GetSpendingAccount).toHaveBeenCalledWith(orderPayments.Items[1].SpendingAccountID); + })); + it('should call LineItems.List',inject(function($injector){ + $injector.invoke(state.resolve.LineItemsList); + expect(oc.LineItems.List).toHaveBeenCalledWith(submittedOrder.ID); + })); + it('should call LineItemHelper', inject(function($injector){ + $injector.invoke(state.resolve.LineItemsList); + scope.$digest(); + expect(lineItemHelpers.GetProductInfo).toHaveBeenCalled(); + })); + }); + + describe('Controller: ConfirmationCtrl', function(){ + var confirmCtrl, + SubmittedOrder = 'FAKE_ORDER', + OrderShipAddress = 'FAKE_SHIP_ADDRESS', + OrderPromotions = {Items: 'FAKE_PROMOTIONS'}, + OrderBillingAddress = 'FAKE_BILL_ADDRESS', + OrderPayments = {Items: 'FAKE_PAYMENTS'}, + LineItemsList = 'FAKE_LINE_ITEMS'; + beforeEach(inject(function($controller) { + confirmCtrl = $controller('CheckoutConfirmationCtrl', { + SubmittedOrder: SubmittedOrder, + OrderShipAddress: OrderShipAddress, + OrderPromotions: OrderPromotions, + OrderBillingAddress: OrderBillingAddress, + OrderPayments: OrderPayments, + LineItemsList: LineItemsList + }); + })); + it ('should initialize the resolves into the controller view model', function() { + expect(confirmCtrl.order).toBe(SubmittedOrder); + expect(confirmCtrl.shippingAddress).toBe(OrderShipAddress); + expect(confirmCtrl.promotions).toBe('FAKE_PROMOTIONS'); + expect(confirmCtrl.billingAddress).toBe(OrderBillingAddress); + expect(confirmCtrl.payments).toBe('FAKE_PAYMENTS'); + expect(confirmCtrl.lineItems).toBe(LineItemsList); + }); + }); +}); \ No newline at end of file diff --git a/src/app/checkout/tests/checkout.payment.spec.js b/src/app/checkout/tests/checkout.payment.spec.js new file mode 100644 index 00000000..d53093bb --- /dev/null +++ b/src/app/checkout/tests/checkout.payment.spec.js @@ -0,0 +1,119 @@ +describe('Component: Checkout Payment', function() { + var scope, + q, + oc, + order, + address, + paymentListItems; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function ($q, $rootScope, OrderCloud) { + q = $q; + oc = OrderCloud; + scope = $rootScope.$new(); + order = { + ID: 'TestOrder123456789', + Type: 'Standard', + FromUserID: 'TestUser123456789', + BillingAddressID: 'TestAddress123456789', + BillingAddress: { + ID: 'TestAddress123456789' + }, + ShippingAddressID: 'TestAddress123456789', + Comments: null, + ShippingCost: null, + TaxCost: null, + Subtotal: 10, + Total: 10 + }; + address = { + ID: 'TestAddress123456789' + }; + paymentListItems = { + Items: [{Type: 'CreditCard', CreditCardID: 'CC123', Amount: 5}, {Type: 'SpendingAccount', SpendingAccountID: 'SA123', Amount: 5}], + Meta: { + Page: 1, + PageSize: 20, + TotalCount: 0, + TotalPages: 1, + ItemRange: [1, 0] + } + }; + })); + + describe('Controller: CheckoutPaymentCtrl', function() { + var checkoutPaymentCtrl, + addressSelectModal, + myAddressModal, + rootScope; + beforeEach(inject(function($rootScope, $controller, AddressSelectModal, MyAddressesModal) { + addressSelectModal = AddressSelectModal; + myAddressModal = MyAddressesModal; + checkoutPaymentCtrl = $controller('CheckoutPaymentCtrl'); + rootScope = $rootScope; + })); + + describe('Function: createAddress', function() { + beforeEach(function() { + var df = q.defer(); + df.resolve({ID:'ADDRESS_ID'}); + spyOn(myAddressModal, 'Create').and.returnValue(df.promise); + + var defer = q.defer(); + defer.resolve('UPDATED_ORDER'); + spyOn(oc.Orders, 'Patch').and.returnValue(defer.promise); + + spyOn(rootScope, '$broadcast').and.callThrough(); + + checkoutPaymentCtrl.createAddress(order); + }); + it ('should call MyAddressModal.Create()', function() { + expect(myAddressModal.Create).toHaveBeenCalled(); + }); + it ('should set the order.BillingAddressID to the new address ID', function() { + scope.$digest(); + expect(order.BillingAddressID).toBe('ADDRESS_ID'); + }); + it ('should patch the order with the new order.BillingAddressID', function() { + scope.$digest(); + expect(oc.Orders.Patch).toHaveBeenCalledWith(order.ID, {BillingAddressID: 'ADDRESS_ID'}); + }); + it ('should broadcast a $rootScope event "OC:OrderBillAddressUpdated"', function() { + scope.$digest(); + expect(rootScope.$broadcast).toHaveBeenCalledWith('OC:OrderBillAddressUpdated', 'UPDATED_ORDER'); + }) + }); + + describe('Function: changeBillingAddress', function() { + beforeEach(function() { + var df = q.defer(); + df.resolve({ID:'ADDRESS_ID'}); + spyOn(addressSelectModal, 'Open').and.returnValue(df.promise); + + var defer = q.defer(); + defer.resolve('UPDATED_ORDER'); + spyOn(oc.Orders, 'Patch').and.returnValue(defer.promise); + + spyOn(rootScope, '$broadcast').and.callThrough(); + + checkoutPaymentCtrl.changeBillingAddress(order); + }); + it ('should call AddressSelectModal.Open() with "billing"', function() { + expect(addressSelectModal.Open).toHaveBeenCalledWith('billing'); + + }); + it ('should set the order.BillingAddressID to the new address ID', function() { + scope.$digest(); + expect(order.BillingAddressID).toBe('ADDRESS_ID'); + }); + it ('should patch the order with the new order.BillingAddressID', function() { + scope.$digest(); + expect(oc.Orders.Patch).toHaveBeenCalledWith(order.ID, {BillingAddressID: 'ADDRESS_ID'}); + }); + it ('should broadcast a $rootScope event "OC:OrderBillAddressUpdated"', function() { + scope.$digest(); + expect(rootScope.$broadcast).toHaveBeenCalledWith('OC:OrderBillAddressUpdated', 'UPDATED_ORDER'); + }) + }); + }); +}); \ No newline at end of file diff --git a/src/app/checkout/tests/checkout.review.spec.js b/src/app/checkout/tests/checkout.review.spec.js new file mode 100644 index 00000000..a47ffae4 --- /dev/null +++ b/src/app/checkout/tests/checkout.review.spec.js @@ -0,0 +1,124 @@ +describe('Component: Checkout Review', function() { + var scope, + q, + oc, + lineItemHelpers, + lineItemsList, + order, + currentOrder, + orderPayments; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(module(function($provide) { + $provide.value('CurrentOrder', { + ID: 'CurrentOrder123', + BillingAddressID: 'TestAddress123456789', + ShippingAddressID: 'TestAddress123456789', + BillingAddress: { + ID: 'TestAddress123456789' + } + }); + $provide.value('OrderPayments', { + Items: [{Type: 'CreditCard', CreditCardID: 'CC123'}, {Type: 'SpendingAccount', SpendingAccountID: 'SA123'}], + Meta: { + Page: 1, + PageSize: 20, + TotalCount: 0, + TotalPages: 1, + ItemRange: [1, 0] + } + }); + })); + beforeEach(inject(function($q, $rootScope, OrderCloud, CurrentOrder, OrderPayments, ocLineItems) { + q = $q; + oc = OrderCloud; + scope = $rootScope.$new(); + lineItemHelpers = ocLineItems; + currentOrder = CurrentOrder; + orderPayments = OrderPayments; + order = { + ID: 'TestOrder123456789', + Type: 'Standard', + FromUserID: 'TestUser123456789', + BillingAddressID: 'TestAddress123456789', + BillingAddress: { + ID: 'TestAddress123456789' + }, + ShippingAddressID: 'TestAddress123456789', + Comments: null, + ShippingCost: null, + TaxCost: null, + Subtotal: 10, + Total: 10 + }; + lineItemsList = { + Items : [{ID: '1'}, {ID: '2'}], + Meta : { + Page: 1, + PageSize: 20, + TotalCount: 2, + TotalPages: 1, + ItemRange: [1,2] + } + }; + })); + + describe('State: checkout.review', function() { + var state; + beforeEach(inject(function($state) { + state = $state.get('checkout.review'); + var lineItemListDefer = q.defer(); + lineItemListDefer.resolve(lineItemsList); + spyOn(oc.LineItems, 'List').and.returnValue(lineItemListDefer.promise); + spyOn(lineItemHelpers, 'GetProductInfo').and.returnValue(lineItemListDefer.promise); + + var defer = q.defer(); + defer.resolve(); + var paymentsDefer = q.defer(); + paymentsDefer.resolve(orderPayments); + spyOn(oc.Payments, 'List').and.returnValue(paymentsDefer.promise); + spyOn(oc.Me, 'GetCreditCard').and.returnValue(defer.promise); + spyOn(oc.Me, 'GetSpendingAccount').and.returnValue(defer.promise); + })); + it('should call LineItems.List', inject(function($injector){ + $injector.invoke(state.resolve.LineItemsList); + expect(oc.LineItems.List).toHaveBeenCalledWith(currentOrder.ID); + })); + it('should call LineItemHelper', inject(function($injector){ + $injector.invoke(state.resolve.LineItemsList); + scope.$digest(); + expect(lineItemHelpers.GetProductInfo).toHaveBeenCalled(); + })); + it('should call Payment List method', inject(function($injector) { + $injector.invoke(state.resolve.OrderPaymentsDetail); + scope.$digest(); + expect(oc.Payments.List).toHaveBeenCalledWith(currentOrder.ID); + })); + it('should call Me.GetCreditCard for first payment', inject(function($injector) { + $injector.invoke(state.resolve.OrderPaymentsDetail); + scope.$digest(); + expect(oc.Me.GetCreditCard).toHaveBeenCalledWith(orderPayments.Items[0].CreditCardID); + })); + it('should call Me.GetSpendingAccount for second payment', inject(function($injector) { + $injector.invoke(state.resolve.OrderPaymentsDetail); + scope.$digest(); + expect(oc.Me.GetSpendingAccount).toHaveBeenCalledWith(orderPayments.Items[1].SpendingAccountID); + })); + }); + + describe('Controller: CheckoutReviewCtrl', function(){ + var reviewCtrl, + OrderPaymentsDetail = 'FAKE_PAYMENTS', + LineItemsList = 'FAKE_LINE_ITEMS'; + beforeEach(inject(function($controller) { + reviewCtrl = $controller('CheckoutReviewCtrl', { + OrderPaymentsDetail: OrderPaymentsDetail, + LineItemsList: LineItemsList + }); + })); + it ('should initialize the resolves into the controller view model', function() { + expect(reviewCtrl.payments).toBe(OrderPaymentsDetail); + expect(reviewCtrl.lineItems).toBe(LineItemsList); + }); + }); +}); \ No newline at end of file diff --git a/src/app/checkout/tests/checkout.shipping.spec.js b/src/app/checkout/tests/checkout.shipping.spec.js new file mode 100644 index 00000000..ee4b21df --- /dev/null +++ b/src/app/checkout/tests/checkout.shipping.spec.js @@ -0,0 +1,247 @@ +describe('Component: Checkout Shipping', function() { + var scope, + rootScope, + q, + oc, + order, + checkoutConfig, + address, + rates; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(module(function($provide) { + $provide.value('CheckoutConfig', { + ShippingRates: true, + TaxRates: false + }); + })); + beforeEach(inject(function($q, $rootScope, OrderCloud, CheckoutConfig) { + q = $q; + oc = OrderCloud; + scope = $rootScope.$new(); + rootScope = $rootScope; + checkoutConfig = CheckoutConfig; + order = { + ID: 'TestOrder123456789', + Type: 'Standard', + FromUserID: 'TestUser123456789', + BillingAddressID: 'TestAddress123456789', + BillingAddress: { + ID: 'TestAddress123456789' + }, + ShippingAddressID: 'TestAddress123456789', + Comments: null, + ShippingCost: null, + TaxCost: null, + Subtotal: 10, + Total: 10 + }; + address = { + ID: 'TestAddress123456789' + }; + rates = { + Shipments: [ + { + Weight: 10, + ShipFromAddressID: '1234', + ShipToAddressID: '2345', + LineItemIDs: [ + '1', + '2' + ], + Rates: [ + { + Price: 6, + Description: 'UPS Standard' + }, + { + Price: 20, + Description: 'UPS Next Day Air' + } + ], + SelectedShipper: { + Price: 6, + Description: 'UPS Standard' + } + } + ] + }; + })); + + describe('Controller: CheckoutShippingCtrl', function() { + var checkoutShippingController, + toaster, + myAddressesModal, + addressSelectModal, + shippingRates, + mockModal; + beforeEach(inject(function($state, $controller, toastr, MyAddressesModal, AddressSelectModal, ShippingRates) { + toaster = toastr; + state = $state; + myAddressesModal = MyAddressesModal; + addressSelectModal = AddressSelectModal; + shippingRates = ShippingRates; + + mockModal = { + result: { + then: function(confirmCallBack, cancelCallBack) { + this.confirmCallBack = confirmCallBack; + this.cancelCallBack = cancelCallBack; + } + }, + close: function(item) { + this.result.confirmCallBack(item); + }, + dismiss: function(type) { + this.result.cancelCallBack(type); + } + }; + + checkoutShippingController = $controller('CheckoutShippingCtrl', { + $scope: scope + }); + + var defer = q.defer(); + defer.resolve(); + + var orderDefer = q.defer(); + orderDefer.resolve(order); + + var shippingRatesDefer = q.defer(); + shippingRatesDefer.resolve(rates.Shipments); + + spyOn(myAddressesModal, 'Create').and.returnValue(mockModal.result); + spyOn(toaster, 'success'); + spyOn(addressSelectModal, 'Open').and.returnValue(mockModal.result); + spyOn(oc.Orders, 'Patch').and.returnValue(orderDefer.promise); + spyOn(rootScope, '$broadcast').and.returnValue(true); + spyOn(shippingRates, 'GetRates').and.returnValue(shippingRatesDefer.promise); + spyOn(shippingRates, 'AnalyzeShipments').and.returnValue(shippingRatesDefer.promise); + spyOn(shippingRates, 'ManageShipments').and.returnValue(defer.promise); + })); + + describe('createAddress', function() { + beforeEach(inject(function() { + checkoutShippingController.createAddress(order); + scope.$digest(); + })); + it('should call MyAddressModal Create method', function() { + expect(myAddressesModal.Create).toHaveBeenCalled(); + }); + it('should call toastr success after address is created', function() { + mockModal.close(address); + expect(toaster.success).toHaveBeenCalled(); + }); + it('should call saveShipAddress which patches order', function() { + checkoutShippingController.saveShipAddress(order); + expect(oc.Orders.Patch).toHaveBeenCalledWith(order.ID, {ShippingAddressID: order.ShippingAddressID}); + }) + }); + + describe('changeShippingAddress', function() { + beforeEach(function() { + checkoutShippingController.changeShippingAddress(order); + scope.$digest(); + }); + it('should call AddressSelectModal Open method with "shipping"', function() { + expect(addressSelectModal.Open).toHaveBeenCalledWith('shipping'); + }); + it('should call createAddress if "create" is returned', function() { + mockModal.close('create'); + checkoutShippingController.createAddress(order); + expect(myAddressesModal.Create).toHaveBeenCalled(); + }); + it('should call saveShipAddress if nothing address is returned', function() { + mockModal.close(address); + checkoutShippingController.saveShipAddress(order); + expect(oc.Orders.Patch).toHaveBeenCalledWith(order.ID, {ShippingAddressID: address.ID}); + }); + }); + + describe('saveShipAddress', function() { + beforeEach(function() { + checkoutShippingController.saveShipAddress(order); + scope.$digest(); + }); + it('should call Orders Patch method', function() { + expect(oc.Orders.Patch).toHaveBeenCalledWith(order.ID, {ShippingAddressID: order.ShippingAddressID}); + }); + it('should broadcast "OC:OrderShipAddressUpdates"', function() { + expect(rootScope.$broadcast).toHaveBeenCalledWith('OC:OrderShipAddressUpdated', order); + }); + it('should call getShippingRates', function() { + expect(shippingRates.GetRates).toHaveBeenCalledWith(order); + }); + }); + + describe('initShippingRates', function() { + beforeEach(function() { + checkoutShippingController.initShippingRates(order); + scope.$digest(); + }); + it('should call getShippingRates', function() { + expect(shippingRates.GetRates).toHaveBeenCalledWith(order); + }); + }); + + describe('getShippingRates', function() { + beforeEach(function() { + checkoutShippingController.getShippingRates(order); + scope.$digest(); + }); + it('should call ShippingRates GetRates method', function() { + expect(shippingRates.GetRates).toHaveBeenCalledWith(order); + }); + it('should call analyzeShipments', function() { + expect(shippingRates.AnalyzeShipments).toHaveBeenCalledWith(order, rates.Shipments); + }); + }); + + describe('shipperSelected', function() { + beforeEach(function() { + checkoutShippingController.shippingRates = rates.Shipments; + checkoutShippingController.shipperSelected(order); + scope.$digest(); + }); + it('should call ShippingRates ManageShipments method', function() { + expect(shippingRates.ManageShipments).toHaveBeenCalledWith(order, rates.Shipments); + }); + it('should broadcast "OC:UpdateOrder"', function() { + expect(rootScope.$broadcast).toHaveBeenCalledWith('OC:UpdateOrder', order.ID); + }); + }); + }); + + describe('Factory: ShippingRates', function() { + var resource, apiurl, shippingratesurl, shippingRates, httpBackend; + beforeEach(inject(function($resource, _apiurl_, ShippingRates, $httpBackend) { + resource = $resource; + apiurl = _apiurl_; + shippingRates = ShippingRates; + httpBackend = $httpBackend; + shippingratesurl = apiurl + '/v1/integrationproxy/shippingrates'; + var defer = q.defer(); + defer.resolve(); + })); + + it('should have a GetRates method', function() { + expect(typeof shippingRates.GetRates).toBe('function'); + }); + + it('should have a GetLineItemRates method', function() { + expect(typeof shippingRates.GetLineItemRates).toBe('function'); + }); + + it('should have a SetShippingCost method', function() { + expect(typeof shippingRates.SetShippingCost).toBe('function'); + }); + + it('should have a ManageShipments method', function() { + expect(typeof shippingRates.ManageShipments).toBe('function'); + }); + + it('should have a AnalyzeShipments method', function() { + expect(typeof shippingRates.AnalyzeShipments).toBe('function'); + }); + }); +}); \ No newline at end of file diff --git a/src/app/checkout/tests/checkout.spec.js b/src/app/checkout/tests/checkout.spec.js new file mode 100644 index 00000000..6e15e9c3 --- /dev/null +++ b/src/app/checkout/tests/checkout.spec.js @@ -0,0 +1,297 @@ +describe('Component: Checkout', function() { + var scope, + q, + oc, + lineItemHelpers, + order, + currentOrder, + submittedOrder, + lineItemsList, + paymentListEmpty, + paymentListItems, + orderPayments, + orderPromotions, + address; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(module(function($provide) { + $provide.value('CurrentOrder', { + ID: 'CurrentOrder123', + BillingAddressID: 'TestAddress123456789', + ShippingAddressID: 'TestAddress123456789', + BillingAddress: { + ID: 'TestAddress123456789' + } + }); + $provide.value('OrderPayments', { + Items: [{Type: 'CreditCard', CreditCardID: 'CC123'}, {Type: 'SpendingAccount', SpendingAccountID: 'SA123'}], + Meta: { + Page: 1, + PageSize: 20, + TotalCount: 0, + TotalPages: 1, + ItemRange: [1, 0] + } + }); + $provide.value('SubmittedOrder', { + ID: 'SubmittedOrder123', + BillingAddressID: 'TestAddress123456789', + ShippingAddressID: 'TestAddress123456789', + BillingAddress: { + ID: 'TestAddress123456789' + } + }); + })); + beforeEach(inject(function($q, $rootScope, OrderCloud, ocLineItems, CurrentOrder, OrderPayments, SubmittedOrder) { + q = $q; + oc = OrderCloud; + lineItemHelpers = ocLineItems; + scope = $rootScope.$new(); + currentOrder = CurrentOrder; + submittedOrder = SubmittedOrder; + orderPayments = OrderPayments; + order = { + ID: 'TestOrder123456789', + Type: 'Standard', + FromUserID: 'TestUser123456789', + BillingAddressID: 'TestAddress123456789', + BillingAddress: { + ID: 'TestAddress123456789' + }, + ShippingAddressID: 'TestAddress123456789', + Comments: null, + ShippingCost: null, + TaxCost: null, + Subtotal: 10, + Total: 10 + }; + lineItemsList = { + Items : [{ID: '1'}, {ID: '2'}], + Meta : { + Page: 1, + PageSize: 20, + TotalCount: 2, + TotalPages: 1, + ItemRange: [1,2] + } + }; + paymentListEmpty = { + Items: [], + Meta: { + Page: 1, + PageSize: 20, + TotalCount: 0, + TotalPages: 1, + ItemRange: [1, 0] + } + }; + paymentListItems = { + Items: [{Type: 'CreditCard', CreditCardID: 'CC123', Amount: 5}, {Type: 'SpendingAccount', SpendingAccountID: 'SA123', Amount: 5}], + Meta: { + Page: 1, + PageSize: 20, + TotalCount: 0, + TotalPages: 1, + ItemRange: [1, 0] + } + }; + orderPromotions = { + Items: [], + Meta: { + Page: 1, + PageSize: 20, + TotalCount: 0, + TotalPages: 1, + ItemRange: [1, 0] + } + }; + address = { + ID: 'TestAddress123456789' + }; + })); + + describe('State: checkout', function() { + var state; + beforeEach(inject(function($state) { + state = $state.get('checkout'); + var defer = q.defer(); + defer.resolve(); + spyOn(oc.Me, 'GetAddress').and.returnValue(defer.promise); + spyOn(oc.Orders, 'ListPromotions').and.returnValue(defer.promise); + + var paymentsDefer = q.defer(); + paymentsDefer.resolve(paymentListEmpty); + spyOn(oc.Payments, 'List').and.returnValue(paymentsDefer.promise); + spyOn(oc.Payments, 'Create').and.returnValue(paymentsDefer.promise); + })); + it('should call Me.GetAddress for ShippingAddressID', inject(function($injector) { + $injector.invoke(state.resolve.OrderShipAddress); + expect(oc.Me.GetAddress).toHaveBeenCalledWith(currentOrder.ShippingAddressID); + })); + it('should call Orders.ListPromotions', inject(function($injector) { + $injector.invoke(state.resolve.CurrentPromotions); + expect(oc.Orders.ListPromotions).toHaveBeenCalledWith(currentOrder.ID); + })); + it('should call Me.GetAddress for BillingAddressID', inject(function($injector) { + $injector.invoke(state.resolve.OrderBillingAddress); + expect(oc.Me.GetAddress).toHaveBeenCalledWith(currentOrder.BillingAddressID); + })); + }); + + describe('Controller: CheckoutController', function() { + var checkoutController, toaster; + beforeEach(inject(function($state, $controller, toastr) { + toaster = toastr; + state = $state; + checkoutController = $controller('CheckoutCtrl', { + $scope: scope, + OrderShipAddress: address, + OrderBillingAddress: address, + CurrentPromotions: orderPromotions, + OrderPayments: paymentListItems + }); + var orderDefer = q.defer(); + orderDefer.resolve(order); + spyOn(oc.Orders, 'Submit').and.returnValue(orderDefer.promise); + + var defer = q.defer(); + defer.resolve(order); + spyOn(toaster, 'success'); + spyOn($state, 'go').and.returnValue(true); + spyOn($state, 'transitionTo').and.returnValue(true); + + var addressDefer = q.defer(); + addressDefer.resolve(address); + + spyOn(oc.Me, 'GetAddress').and.returnValue(addressDefer.promise); + + spyOn(oc.Payments, 'List').and.returnValue(defer.promise); + + spyOn(oc.Orders, 'RemovePromotion').and.returnValue(defer.promise); + + spyOn(oc.Orders, 'ListPromotions').and.returnValue(defer.promise); + })); + + describe('submitOrder', function() { + beforeEach(function() { + checkoutController.submitOrder(order); + scope.$digest(); + }); + it('should call Orders Submit method', function(){ + expect(oc.Orders.Submit).toHaveBeenCalledWith(order.ID); + }); + it('should go to confirmation state', function() { + expect(state.go).toHaveBeenCalledWith('confirmation', {orderid: order.ID}, {reload: 'base'}); + }); + it('should call toastr when successful', function(){ + expect(toaster.success).toHaveBeenCalled(); + }); + }); + + describe('OC:OrderShipAddressUpdated', function() { + it('should call Me GetAddress method on broadcasted order ShippingAddressID', inject(function($rootScope) { + $rootScope.$broadcast('OC:OrderShipAddressUpdated', order); + scope.$digest(); + expect(oc.Me.GetAddress).toHaveBeenCalledWith(order.ShippingAddressID); + })); + it('should update checkoutController.shippingAddress to new address', inject(function($rootScope) { + checkoutController.shippingAddress = {ID: 'SA123'}; + expect(checkoutController.shippingAddress.ID).toEqual('SA123'); + $rootScope.$broadcast('OC:OrderShipAddressUpdated', order); + scope.$digest(); + expect(checkoutController.shippingAddress.ID).toEqual('TestAddress123456789'); + })); + }); + + describe('OC:OrderBillAddressUpdated', function() { + it('should call Me GetAddress method on broadcasted order BillingAddressID', inject(function($rootScope) { + $rootScope.$broadcast('OC:OrderBillAddressUpdated', order); + scope.$digest(); + expect(oc.Me.GetAddress).toHaveBeenCalledWith(order.BillingAddressID); + })); + it('should update checkoutController.billingAddress to new address', inject(function($rootScope) { + checkoutController.billingAddress = {ID: 'BA123'}; + expect(checkoutController.billingAddress.ID).toEqual('BA123'); + $rootScope.$broadcast('OC:OrderBillAddressUpdated', order); + scope.$digest(); + expect(checkoutController.billingAddress.ID).toEqual('TestAddress123456789'); + })); + }); + + describe('removePromotion', function() { + it('should call Orders RemovePromotion method', function() { + checkoutController.removePromotion(order, {Code: 'Promo123'}); + scope.$digest(); + expect(oc.Orders.RemovePromotion).toHaveBeenCalledWith(order.ID, 'Promo123'); + }); + }); + + describe('OC:UpdatePromotions', function() { + it('should call Orders ListPromotions method on broadcasted orderid', inject(function($rootScope) { + $rootScope.$broadcast('OC:UpdatePromotions', order.ID); + scope.$digest(); + expect(oc.Orders.ListPromotions).toHaveBeenCalledWith(order.ID); + })); + }); + }); + + describe('Factory: AddressSelectModal', function() { + var addressSelectModal, uibModal; + beforeEach(inject(function($uibModal, _AddressSelectModal_) { + uibModal = $uibModal; + addressSelectModal = _AddressSelectModal_; + var defer = q.defer(); + defer.resolve(); + spyOn(uibModal, 'open').and.returnValue(defer.promise); + })); + + it('should have an Open method', function() { + expect(typeof addressSelectModal.Open).toBe('function'); + }); + + it('should call $uibModal.open when Open method is called', function() { + addressSelectModal.Open(); + scope.$digest(); + expect(uibModal.open).toHaveBeenCalled(); + }); + }); + + describe('Controller: AddressSelectCtrl', function() { + var addressSelectCtrl, uibModalInstance; + beforeEach(inject(function($controller) { + uibModalInstance = {close: function() {}, dismiss: function() {}};; + addressSelectCtrl = $controller('AddressSelectCtrl', { + $scope: scope, + $uibModalInstance: uibModalInstance, + Addresses: {Items: [], Meta: {}} + }); + spyOn(uibModalInstance, 'close').and.returnValue(); + spyOn(uibModalInstance, 'dismiss').and.returnValue(); + })); + + describe('select', function() { + it('should call $uibmodalInstance.close with selected address', function() { + addressSelectCtrl.select(address); + scope.$digest(); + expect(uibModalInstance.close).toHaveBeenCalledWith(address); + }); + }); + + describe('createAddress', function() { + it('should call $uibmodalInstance.close with "create"', function() { + addressSelectCtrl.createAddress(); + scope.$digest(); + expect(uibModalInstance.close).toHaveBeenCalledWith('create'); + }); + }); + + describe('cancel', function() { + it('should call $uibmodalInstance.dismiss', function() { + addressSelectCtrl.cancel(); + scope.$digest(); + expect(uibModalInstance.dismiss).toHaveBeenCalled(); + }); + }); + }); +}); + diff --git a/src/app/checkout/tests/checkout.test.js b/src/app/checkout/tests/checkout.test.js new file mode 100644 index 00000000..e69de29b diff --git a/src/app/checkout/tests/payments.spec.js b/src/app/checkout/tests/payments.spec.js new file mode 100644 index 00000000..5989cf7b --- /dev/null +++ b/src/app/checkout/tests/payments.spec.js @@ -0,0 +1,473 @@ +describe('Payment Directives', function() { + var oc, + q, + scope, + rootScope; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($q, $rootScope, OrderCloud) { + oc = OrderCloud; + q = $q; + scope = $rootScope.$new(); + rootScope = $rootScope; + })); + + describe('Controller: PaymentPurchaseOrderCtrl', function() { + var paymentPOCtrl, + order = {ID:'ORDER_ID'}, + payment = {ID:'PAYMENT_ID', Type:'OtherPaymentType'}, + controller; + + beforeEach(inject(function($controller) { + controller = $controller; + })); + + describe('Initalize payment details', function() { + describe('When a payment is not passed through', function() { + beforeEach(function() { + scope.payment = undefined; + scope.order = order; + }); + it ('should list payments', function() { + spyOn(oc.Payments, 'List').and.callThrough(); + paymentPOCtrl = controller('PaymentPurchaseOrderCtrl', { + $scope: scope + }); + expect(oc.Payments.List).toHaveBeenCalledWith(order.ID); + }); + it ('should update the first existing payment to a purchase order', function() { + var existingpayments = q.defer(); + existingpayments.resolve({Items: [{ID: 'EXISTING_PAYMENT_ID'}]}); + spyOn(oc.Payments, 'List').and.returnValue(existingpayments.promise); + + var df = q.defer(); + df.resolve('UPDATED_PAYMENT'); + spyOn(oc.Payments, 'Patch').and.returnValue(df.resolve); + + paymentPOCtrl = controller('PaymentPurchaseOrderCtrl', { + $scope: scope + }); + scope.$digest(); + expect(oc.Payments.Patch).toHaveBeenCalledWith(order.ID, 'EXISTING_PAYMENT_ID', { + Type: "PurchaseOrder", + CreditCardID: null, + SpendingAccountID: null, + Amount: null + }); + }); + it ('should create a new payment if one does not exist', function() { + var nopayments = q.defer(); + nopayments.resolve({Items:[]}); + spyOn(oc.Payments, 'List').and.returnValue(nopayments.promise); + + var df = q.defer(); + df.resolve("NEW_PAYMENT"); + spyOn(oc.Payments, 'Create').and.returnValue(df.resolve); + + paymentPOCtrl = controller('PaymentPurchaseOrderCtrl', { + $scope: scope + }); + scope.$digest(); + expect(oc.Payments.Create).toHaveBeenCalledWith(order.ID, {Type: "PurchaseOrder"}); + //scope.$digest(); TODO: make this expect statement work + //expect(paymentPOCtrl.payment).toBe("NEW_PAYMENT"); + }) + }); + describe('When a payment is passed through', function() { + it ('should update payment to a purchase order', function() { + scope.payment = payment; + scope.order = order; + + var defer = q.defer(); + defer.resolve('UPDATED_PAYMENT'); + spyOn(oc.Payments, 'Patch').and.returnValue(defer.promise); + + paymentPOCtrl = controller('PaymentPurchaseOrderCtrl', { + $scope: scope + }); + + expect(oc.Payments.Patch).toHaveBeenCalledWith(order.ID, payment.ID, { + ID: payment.ID, + SpendingAccountID:null, + CreditCardID:null, + Type:"PurchaseOrder" + }); + }); + }); + }); + + describe('$scope.updatePayment()', function() { + it ('Should patch the current payment to a purchase order', function() { + scope.payment = payment; + scope.order = order; + paymentPOCtrl = controller('PaymentPurchaseOrderCtrl', { + $scope: scope + }); + var updatedpayment = q.defer(); + updatedpayment.resolve(); + spyOn(oc.Payments, 'Update').and.returnValue(updatedpayment.promise); + spyOn(rootScope, '$broadcast').and.callThrough(); + scope.updatePayment(); + expect(oc.Payments.Update).toHaveBeenCalledWith(order.ID, payment.ID, payment); + scope.$digest(); + expect(rootScope.$broadcast).toHaveBeenCalledWith('OC:PaymentsUpdated'); + }) + }) + }); + + describe('Controller: PaymentSpendingAccountCtrl', function() { + var paymentPOCtrl, + order = {ID:'ORDER_ID'}, + payment = {ID:'PAYMENT_ID', Type:'OtherPaymentType'}, + controller; + + beforeEach(inject(function($controller) { + controller = $controller; + scope.OCPaymentSpendingAccount = { + $error: {}, + $valid:true, + $invalid:false, + $setValidity:(function(error, bool){ + scope.OCPaymentSpendingAccount.$error[error] = []; + scope.OCPaymentSpendingAccount.$valid = bool; + scope.OCPaymentSpendingAccount.$invalid = !bool; + return null; + }) + }; + })); + + describe('Get Spending Accounts', function() { + it ('should list the first 100 non-redemption code spending accounts', function() { + scope.payment = payment; + scope.order = order; + + var data = { + Items:['SpendingAccount1', 'SpendingAccount2'] + }; + + var df = q.defer(); + df.resolve(data); + spyOn(oc.Me, 'ListSpendingAccounts').and.returnValue(df.promise); + + paymentPOCtrl = controller('PaymentSpendingAccountCtrl', { + $scope: scope + }); + + expect(oc.Me.ListSpendingAccounts).toHaveBeenCalledWith(null, 1, 100, null, null, {RedemptionCode: '!*', AllowAsPaymentMethod: true}); + scope.$digest(); + expect(scope.spendingAccounts).toBe(data.Items); + }) + }); + + describe('Initalize payment details', function() { + describe('When a payment is not passed through', function() { + beforeEach(function() { + scope.payment = undefined; + scope.order = order; + }); + it ('should list payments', function() { + spyOn(oc.Payments, 'List').and.callThrough(); + paymentPOCtrl = controller('PaymentSpendingAccountCtrl', { + $scope: scope + }); + expect(oc.Payments.List).toHaveBeenCalledWith(order.ID); + }); + it ('should update the first existing payment to a spending account payment', function() { + var existingpayments = q.defer(); + existingpayments.resolve({Items: [{ID: 'EXISTING_PAYMENT_ID'}]}); + spyOn(oc.Payments, 'List').and.returnValue(existingpayments.promise); + + var df = q.defer(); + df.resolve('UPDATED_PAYMENT'); + spyOn(oc.Payments, 'Patch').and.returnValue(df.resolve); + + paymentPOCtrl = controller('PaymentSpendingAccountCtrl', { + $scope: scope + }); + scope.$digest(); + expect(oc.Payments.Patch).toHaveBeenCalledWith(order.ID, 'EXISTING_PAYMENT_ID', { + Type: "SpendingAccount", + xp: { + PONumber: null + }, + CreditCardID: null, + SpendingAccountID: null, + Amount: null + }); + }); + it ('should create a new payment if one does not exist', function() { + var nopayments = q.defer(); + nopayments.resolve({Items:[]}); + spyOn(oc.Payments, 'List').and.returnValue(nopayments.promise); + + var df = q.defer(); + df.resolve("NEW_PAYMENT"); + spyOn(oc.Payments, 'Create').and.returnValue(df.resolve); + + paymentPOCtrl = controller('PaymentSpendingAccountCtrl', { + $scope: scope + }); + scope.$digest(); + expect(oc.Payments.Create).toHaveBeenCalledWith(order.ID, {Type: "SpendingAccount"}); + //scope.$digest(); + //expect(scope.payment).toBe("NEW_PAYMENT"); + }) + }); + describe('When a payment is passed through', function() { + it ('should update payment to a spending account payment', function() { + scope.payment = payment; + scope.order = order; + + paymentPOCtrl = controller('PaymentSpendingAccountCtrl', { + $scope: scope + }); + + expect(scope.payment.CreditCardID).toBeUndefined(); + expect(scope.payment.xp).toBeUndefined(); + expect(scope.showPaymentOptions).toBeTruthy(); + }); + }); + }); + + describe('$scope.changePayment()', function() { + it ('should show the payment options', function() { + scope.payment = payment; + scope.order = order; + paymentPOCtrl = controller('PaymentSpendingAccountCtrl', { + $scope: scope + }); + scope.changePayment(); + expect(scope.showPaymentOptions).toBeTruthy(); + }) + }); + + describe('$scope.updatePayment()', function() { + beforeEach(function() { + scope.payment = payment; + scope.order = order; + }); + it ('should update the current payment with a SpendingAccountID', function() { + paymentPOCtrl = controller('PaymentSpendingAccountCtrl', { + $scope: scope + }); + var updatedpayment = q.defer(); + updatedpayment.resolve(); + spyOn(oc.Payments, 'Update').and.returnValue(updatedpayment.promise); + spyOn(rootScope, '$broadcast').and.callThrough(); + scope.updatePayment({spendingAccount:{ID:"FAKE_SPENDING_ACCOUNT"}}); + expect(oc.Payments.Update).toHaveBeenCalledWith(order.ID, payment.ID, payment); + scope.$digest(); + expect(rootScope.$broadcast).toHaveBeenCalledWith('OC:PaymentsUpdated'); + expect(scope.showPaymentOptions).toBeFalsy(); + expect(scope.payment.SpendingAccountID).toBe("FAKE_SPENDING_ACCOUNT"); + }); + it ('should reset $scope.payment when the update fails', function() { + paymentPOCtrl = controller('PaymentSpendingAccountCtrl', { + $scope: scope + }); + var updatedpayment = q.defer(); + updatedpayment.reject(); + spyOn(oc.Payments, 'Update').and.returnValue(updatedpayment.promise); + spyOn(rootScope, '$broadcast').and.callThrough(); + scope.updatePayment({spendingAccount:{ID:"FAKE_SPENDING_ACCOUNT"}}); + expect(oc.Payments.Update).toHaveBeenCalledWith(order.ID, payment.ID, payment); + scope.$digest(); + expect(rootScope.$broadcast).not.toHaveBeenCalled(); + expect(scope.payment).toBe(payment); + }) + }) + }); + + describe('Controller: PaymentCreditCardCtrl', function() { + var paymentPOCtrl, + order = {ID:'ORDER_ID'}, + payment = {ID:'PAYMENT_ID', Type:'OtherPaymentType'}, + controller; + + beforeEach(inject(function($controller) { + controller = $controller; + scope.OCPaymentCreditCard = { + $error: {}, + $valid:true, + $invalid:false, + $setValidity:(function(error, bool){ + scope.OCPaymentCreditCard.$error[error] = []; + scope.OCPaymentCreditCard.$valid = bool; + scope.OCPaymentCreditCard.$invalid = !bool; + return null; + }) + }; + })); + + describe('Get Credit Cards', function() { + it ('should list the first 100 credit cards assigned to the user', function() { + scope.payment = payment; + scope.order = order; + + var data = { + Items:['CreditCard1', 'CreditCard2'] + }; + + var df = q.defer(); + df.resolve(data); + spyOn(oc.Me, 'ListCreditCards').and.returnValue(df.promise); + + paymentPOCtrl = controller('PaymentCreditCardCtrl', { + $scope: scope + }); + + expect(oc.Me.ListCreditCards).toHaveBeenCalledWith(null, 1, 100, null, null, {}); + scope.$digest(); + expect(scope.creditCards).toBe(data.Items); + }) + }); + + describe('Initalize payment details', function() { + describe('When a payment is not passed through', function() { + beforeEach(function() { + scope.payment = undefined; + scope.order = order; + }); + it ('should list payments', function() { + spyOn(oc.Payments, 'List').and.callThrough(); + paymentPOCtrl = controller('PaymentCreditCardCtrl', { + $scope: scope + }); + expect(oc.Payments.List).toHaveBeenCalledWith(order.ID); + }); + it ('should update the first existing payment to a credit card payment', function() { + var existingpayments = q.defer(); + existingpayments.resolve({Items: [{ID: 'EXISTING_PAYMENT_ID'}]}); + spyOn(oc.Payments, 'List').and.returnValue(existingpayments.promise); + + var df = q.defer(); + df.resolve('UPDATED_PAYMENT'); + spyOn(oc.Payments, 'Patch').and.returnValue(df.resolve); + + paymentPOCtrl = controller('PaymentCreditCardCtrl', { + $scope: scope + }); + scope.$digest(); + expect(oc.Payments.Patch).toHaveBeenCalledWith(order.ID, 'EXISTING_PAYMENT_ID', { + Type: "CreditCard", + xp: { + PONumber: null + }, + SpendingAccountID: null, + Amount: null + }); + }); + it ('should create a new payment if one does not exist', function() { + var nopayments = q.defer(); + nopayments.resolve({Items:[]}); + spyOn(oc.Payments, 'List').and.returnValue(nopayments.promise); + + var df = q.defer(); + df.resolve("NEW_PAYMENT"); + spyOn(oc.Payments, 'Create').and.returnValue(df.resolve); + + paymentPOCtrl = controller('PaymentCreditCardCtrl', { + $scope: scope + }); + scope.$digest(); + expect(oc.Payments.Create).toHaveBeenCalledWith(order.ID, {Type: "CreditCard"}); + }) + }); + describe('When a payment is passed through', function() { + it ('should update payment to a credit card payment', function() { + scope.payment = payment; + scope.order = order; + + paymentPOCtrl = controller('PaymentCreditCardCtrl', { + $scope: scope + }); + + expect(scope.payment.SpendingAccountID).toBeUndefined(); + expect(scope.payment.xp).toBeUndefined(); + expect(scope.showPaymentOptions).toBeTruthy(); + }); + }); + }); + + describe('$scope.changePayment()', function() { + it ('should show the payment options', function() { + scope.payment = payment; + scope.order = order; + paymentPOCtrl = controller('PaymentCreditCardCtrl', { + $scope: scope + }); + scope.changePayment(); + expect(scope.showPaymentOptions).toBeTruthy(); + }) + }); + + describe('$scope.updatePayment()', function() { + beforeEach(function() { + scope.payment = payment; + scope.order = order; + }); + it ('should update the current payment with a CreditCardID', function() { + paymentPOCtrl = controller('PaymentCreditCardCtrl', { + $scope: scope + }); + var updatedpayment = q.defer(); + updatedpayment.resolve(); + spyOn(oc.Payments, 'Update').and.returnValue(updatedpayment.promise); + spyOn(rootScope, '$broadcast').and.callThrough(); + scope.updatePayment({creditCard:{ID:"FAKE_CREDIT_CARD"}}); + expect(oc.Payments.Update).toHaveBeenCalledWith(order.ID, payment.ID, payment); + scope.$digest(); + expect(rootScope.$broadcast).toHaveBeenCalledWith('OC:PaymentsUpdated'); + expect(scope.showPaymentOptions).toBeFalsy(); + expect(scope.payment.CreditCardID).toBe("FAKE_CREDIT_CARD"); + }); + it ('should reset $scope.payment when the update fails', function() { + paymentPOCtrl = controller('PaymentCreditCardCtrl', { + $scope: scope + }); + var updatedpayment = q.defer(); + updatedpayment.reject(); + spyOn(oc.Payments, 'Update').and.returnValue(updatedpayment.promise); + spyOn(rootScope, '$broadcast').and.callThrough(); + scope.updatePayment({creditCard:{ID:"FAKE_CREDIT_CARD"}}); + expect(oc.Payments.Update).toHaveBeenCalledWith(order.ID, payment.ID, payment); + scope.$digest(); + expect(rootScope.$broadcast).not.toHaveBeenCalled(); + expect(scope.payment).toBe(payment); + }) + }); + + describe('$scope.createCreditCard()', function() { + var myPaymentCreditCardModal; + beforeEach(inject(function(MyPaymentCreditCardModal) { + myPaymentCreditCardModal = MyPaymentCreditCardModal; + scope.payment = payment; + scope.order = order; + scope.creditCards = []; + + paymentPOCtrl = controller('PaymentCreditCardCtrl', { + $scope: scope, + MyPaymentCreditCardModal: myPaymentCreditCardModal + }); + var df = q.defer(); + df.resolve("NEW_CREDIT_CARD"); + spyOn(myPaymentCreditCardModal, 'Create').and.returnValue(df.promise); + })); + it ('should call MyPaymentCreditCardModal.Create()', function() { + scope.createCreditCard(); + expect(myPaymentCreditCardModal.Create).toHaveBeenCalled(); + }); + it ('should add the new credit card to $scope.creditCards', function() { + scope.createCreditCard(); + scope.$digest(); + expect(scope.creditCards).toEqual(["NEW_CREDIT_CARD"]); + }); + it ('should call $scope.updatePayment with the new credit card', function() { + spyOn(scope, 'updatePayment').and.callThrough(); + scope.createCreditCard(); + scope.$digest(); + expect(scope.updatePayment).toHaveBeenCalledWith({creditCard:"NEW_CREDIT_CARD"}); + }) + }) + }); +}); \ No newline at end of file diff --git a/src/app/common/config/angular-busy.config.js b/src/app/common/config/angular-busy.config.js new file mode 100644 index 00000000..a26a380c --- /dev/null +++ b/src/app/common/config/angular-busy.config.js @@ -0,0 +1,9 @@ +angular.module('orderCloud') + .config(function(angularBusyDefaults) { + angular.extend(angularBusyDefaults, { + templateUrl:'common/templates/view.loading.tpl.html', + message:null, + wrapperClass: 'indicator-container' + }) + }) +; \ No newline at end of file diff --git a/src/app/common/config/localforage.config.js b/src/app/common/config/localforage.config.js new file mode 100644 index 00000000..1f49ed07 --- /dev/null +++ b/src/app/common/config/localforage.config.js @@ -0,0 +1,12 @@ +angular.module('orderCloud') + .config(LocalForage) +; + +function LocalForage($localForageProvider) { + $localForageProvider.config({ + version: 1.0, + name: 'OrderCloud', + storeName: 'four51', + description: 'Four51 OrderCloud Local Storage' + }); +} \ No newline at end of file diff --git a/src/app/common/directives/confirm-password.js b/src/app/common/directives/confirm-password.js new file mode 100644 index 00000000..29a7f89d --- /dev/null +++ b/src/app/common/directives/confirm-password.js @@ -0,0 +1,32 @@ +angular.module('orderCloud') + .directive('confirmpassword', confirmpassword) +; +//TODO: remove this directive in favor of ui.validate +function confirmpassword() { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, element, attrs, ngModel) { + if (!ngModel) return; + + //watch own value and re-validate on change + scope.$watch(attrs.ngModel, function() { + validate(); + }); + + //watch other value and re-validate on change + attrs.$observe('confirmpassword', function(val) { + validate(); + }); + + var validate = function() { + var val1 = ngModel.$viewValue; + var val2 = attrs.confirmpassword; + + (!val1 || !val2 || val1 === val2) ? ngModel.$setValidity('confirmpassword', true) : ngModel.$setValidity('confirmpassword', false); + }; + } + } +} + + diff --git a/src/app/common/directives/oc-quantity-input.js b/src/app/common/directives/oc-quantity-input.js new file mode 100644 index 00000000..56945795 --- /dev/null +++ b/src/app/common/directives/oc-quantity-input.js @@ -0,0 +1,45 @@ +angular.module('orderCloud') + .directive('ocQuantityInput', OCQuantityInput) + +; + +function OCQuantityInput(toastr, OrderCloud, $rootScope) { + return { + scope: { + product: '=', + lineitem: '=', + label: '@', + order: '=', + onUpdate: '&' + }, + templateUrl: 'common/templates/quantityInput.tpl.html', + replace: true, + link: function (scope) { + if (scope.product){ + scope.item = scope.product; + scope.content = "product" + } + else if(scope.lineitem){ + scope.item = scope.lineitem; + scope.content = "lineitem"; + scope.updateQuantity = function() { + if (scope.item.Quantity > 0) { + OrderCloud.LineItems.Patch(scope.order.ID, scope.item.ID, {Quantity: scope.item.Quantity}) + .then(function (data) { + data.Product = scope.lineitem.Product; + scope.item = data; + scope.lineitem = data; + if (typeof scope.onUpdate === "function") scope.onUpdate(scope.lineitem); + toastr.success('Quantity Updated'); + $rootScope.$broadcast('OC:UpdateOrder', scope.order.ID, 'Calculating Order Total'); + }); + } + } + } + else{ + toastr.error('Please input either a product or lineitem attribute in the directive','Error'); + console.error('Please input either a product or lineitem attribute in the quantityInput directive ') + } + } + } +} diff --git a/src/app/common/filters/fa-creditcard.js b/src/app/common/filters/fa-creditcard.js new file mode 100644 index 00000000..cfdd838a --- /dev/null +++ b/src/app/common/filters/fa-creditcard.js @@ -0,0 +1,32 @@ +angular.module('orderCloud') + .filter('faCreditCard', faCreditCard) +; + +function faCreditCard() { + return function(type) { + var result = 'fa-credit-card-alt'; + switch(type.toLowerCase()) { + case 'visa': + result = 'fa-cc-visa'; + break; + case 'mastercard': + result = 'fa-cc-mastercard'; + break; + case 'american express': + result = 'fa-cc-amex'; + break; + case 'diners club': + result = 'fa-cc-diners-club'; + break; + case 'discover': + result = 'fa-cc-discover'; + break; + case 'jcb': + result = 'fa-cc-jcb'; + break; + default: + result = 'fa-credit-card-alt'; + } + return result; + } +} \ No newline at end of file diff --git a/src/app/common/filters/humanize.js b/src/app/common/filters/humanize.js new file mode 100644 index 00000000..510c9846 --- /dev/null +++ b/src/app/common/filters/humanize.js @@ -0,0 +1,14 @@ +angular.module('orderCloud') + .filter('humanize', humanize) +; + +function humanize() { + return function(string) { + if (!string) return; + + return string + .replace(/([A-Z])/g, ' $1') + .replace(/^./, function(str){ return str.toUpperCase(); }) + .trim(); + } +} \ No newline at end of file diff --git a/src/app/common/filters/oc-address.js b/src/app/common/filters/oc-address.js new file mode 100644 index 00000000..d386c6d5 --- /dev/null +++ b/src/app/common/filters/oc-address.js @@ -0,0 +1,39 @@ +angular.module('orderCloud') + .filter('address', AddressFilter) +; + +function AddressFilter() { + return function(address, option) { + if (!address) return null; + if (option === 'full') { + var result = []; + + //address name + if (address.AddressName) result.push('' + address.AddressName + ''); + + //address first/last + if (address.FirstName || address.LastName) { + result.push((address.FirstName && !address.LastName) ? address.FirstName : ((!address.FirstName && address.LastName) ? address.LastName : (address.FirstName + ' ' + address.LastName))); + } + + //company name + if (address.CompanyName) result.push(address.CompanyName); + + //street 1 (required) + result.push(address.Street1); + + //street 1 (optional) + if (address.Street2) result.push(address.Street2); + + //city, state zip + result.push(address.City + ', ' + address.State + ' ' + address.Zip); + + if (address.Phone) result.push(address.Phone); + + return result.join('
'); + } + else { + return address.Street1 + (address.Street2 ? ', ' + address.Street2 : ''); + } + }; +} \ No newline at end of file diff --git a/src/app/common/services/oc-authnet.js b/src/app/common/services/oc-authnet.js new file mode 100644 index 00000000..aea453e3 --- /dev/null +++ b/src/app/common/services/oc-authnet.js @@ -0,0 +1,82 @@ +angular.module('orderCloud') + .factory('ocAuthNet', AuthorizeNet) +; + +function AuthorizeNet( $q, $resource, OrderCloud) { + return { + 'CreateCreditCard': _createCreateCard, + 'UpdateCreditCard': _updateCreditCard, + 'DeleteCreditCard' : _deleteCreditCard + // 'AuthAndCapture' : _authAndCapture + + }; + + function _createCreateCard(creditCard, buyerID) { + var year = creditCard.ExpirationYear.toString().substring(2,4); + var ExpirationDate = creditCard.ExpirationMonth.concat(year); + + return makeApiCall('POST',{ + 'buyerID' : buyerID ? buyerID : OrderCloud.BuyerID.Get(), + 'TransactionType' : "createCreditCard", + 'CardDetails' : { + 'CardholderName' : creditCard.CardholderName, + 'CardType' : creditCard.CardType, + 'CardNumber' : creditCard.CardNumber, + 'ExpirationDate' : ExpirationDate, + 'CardCode' : creditCard.CardCode + } + }); + } + + function _updateCreditCard(creditCard, buyerID) { + var year = creditCard.ExpirationYear.toString().substring(2,4); + var ExpirationDate = creditCard.ExpirationMonth.concat(year); + + return makeApiCall('POST',{ + 'buyerID' : buyerID ? buyerID : OrderCloud.BuyerID.Get(), + 'TransactionType' : "updateCreditCard", + 'CardDetails' : { + 'CreditCardID' : creditCard.ID, + 'CardholderName' : creditCard.CardholderName, + 'CardType' : creditCard.CardType, + 'CardNumber' : 'XXXX'+ creditCard.PartialAccountNumber, + 'ExpirationDate' : ExpirationDate + } + }); + + } + function _deleteCreditCard(creditCard, buyerID) { + return makeApiCall('POST', { + 'buyerID': buyerID ? buyerID : OrderCloud.BuyerID.Get(), + 'TransactionType': "deleteCreditCard", + 'CardDetails': { + 'CreditCardID': creditCard.ID + } + }); + + } + // function _authAndCapture() { + // + // } + + + function makeApiCall(method, requestBody) { + var apiUrl = 'https://api.ordercloud.io/v1/integrationproxy/authorizenettest'; + var d = $q.defer(); + $resource(apiUrl, null, { + callApi: { + method: method, + headers: { + 'Authorization': 'Bearer ' + OrderCloud.Auth.ReadToken() + } + } + }).callApi(requestBody).$promise + .then(function(data) { + d.resolve(data); + }) + .catch(function(ex) { + d.reject(ex); + }); + return d.promise; + } +} \ No newline at end of file diff --git a/src/app/common/services/oc-confirm.js b/src/app/common/services/oc-confirm.js new file mode 100644 index 00000000..d0d113ff --- /dev/null +++ b/src/app/common/services/oc-confirm.js @@ -0,0 +1,41 @@ +angular.module('orderCloud') + .factory('ocConfirm', OrderCloudConfirmService) + .controller('ConfirmModalCtrl', ConfirmModalController) +; + +function OrderCloudConfirmService($uibModal) { + var service = { + Confirm: _confirm + }; + + function _confirm(message) { + return $uibModal.open({ + animation:false, + backdrop:'static', + templateUrl: 'common/templates/confirm.modal.tpl.html', + controller: 'ConfirmModalCtrl', + controllerAs: 'confirmModal', + size: 'sm', + resolve: { + ConfirmMessage: function() { + return message; + } + } + }).result + } + + return service; +} + +function ConfirmModalController($uibModalInstance, ConfirmMessage) { + var vm = this; + vm.message = ConfirmMessage; + + vm.confirm = function() { + $uibModalInstance.close(); + }; + + vm.cancel = function() { + $uibModalInstance.dismiss(); + }; +} \ No newline at end of file diff --git a/src/app/common/services/oc-creditcard-utility.js b/src/app/common/services/oc-creditcard-utility.js new file mode 100644 index 00000000..36a90833 --- /dev/null +++ b/src/app/common/services/oc-creditcard-utility.js @@ -0,0 +1,70 @@ +angular.module('orderCloud') + .factory('ocCreditCardUtility', CreditCardUtility) +; + + +function CreditCardUtility() { + //return the expirationMonth array and its function + var creditCardUtility = { + ExpirationMonths: [{ + number: 1, + string: '01' + }, { + number: 2, + string: '02' + }, { + number: 3, + string: '03' + }, { + number: 4, + string: '04' + }, { + number: 5, + string: '05' + }, { + number: 6, + string: '06' + }, { + number: 7, + string: '07' + }, { + number: 8, + string: '08' + }, { + number: 9, + string: '09' + }, { + number: 10, + string: '10' + }, { + number: 11, + string: '11' + }, { + number: 12, + string: '12' + }], + ExpirationYears: [], + isLeapYear: function leapYear(year) { + return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0); + }, + CreditCardTypes : [ + 'MasterCard', + 'American Express', + 'Discover', + 'Visa' + ] + }; + + function _ccExpireYears() { + var today = new Date(); + today = today.getFullYear(); + for (var x = today; x < today + 21; x++) { + creditCardUtility.ExpirationYears.push(x); + } + return creditCardUtility.ExpirationYears; + } + + _ccExpireYears(); + + return creditCardUtility; +} diff --git a/src/app/common/services/oc-geography.js b/src/app/common/services/oc-geography.js new file mode 100644 index 00000000..4055f656 --- /dev/null +++ b/src/app/common/services/oc-geography.js @@ -0,0 +1,347 @@ +angular.module('orderCloud') + .factory('ocGeography', OCGeography) +; + +function OCGeography() { + var _countries = [ + { "label": "United States of America", "value": "US"}, + { "label": "Afghanistan", "value": "AF"}, + { "label": "Åland Islands", "value": "AX"}, + { "label": "Albania", "value": "AL"}, + { "label": "Algeria", "value": "DZ"}, + { "label": "American Samoa", "value": "AS"}, + { "label": "Andorra", "value": "AD"}, + { "label": "Angola", "value": "AO"}, + { "label": "Anguilla", "value": "AI"}, + { "label": "Antarctica", "value": "AQ"}, + { "label": "Antigua and Barbuda", "value": "AG"}, + { "label": "Argentina", "value": "AR"}, + { "label": "Armenia", "value": "AM"}, + { "label": "Aruba", "value": "AW"}, + { "label": "Australia", "value": "AU"}, + { "label": "Austria", "value": "AT"}, + { "label": "Azerbaijan", "value": "AZ"}, + { "label": "Bahamas", "value": "BS"}, + { "label": "Bahrain", "value": "BH"}, + { "label": "Bangladesh", "value": "BD"}, + { "label": "Barbados", "value": "BB"}, + { "label": "Belarus", "value": "BY"}, + { "label": "Belgium", "value": "BE"}, + { "label": "Belize", "value": "BZ"}, + { "label": "Benin", "value": "BJ"}, + { "label": "Bermuda", "value": "BM"}, + { "label": "Bhutan", "value": "BT"}, + { "label": "Bolivia", "value": "BO"}, + { "label": "Bosnia and Herzegovina", "value": "BA"}, + { "label": "Botswana", "value": "BW"}, + { "label": "Bouvet Island", "value": "BV"}, + { "label": "Brazil", "value": "BR"}, + { "label": "British Indian Ocean Territory", "value": "IO"}, + { "label": "Brunei Darussalam", "value": "BN"}, + { "label": "Bulgaria", "value": "BG"}, + { "label": "Burkina Faso", "value": "BF"}, + { "label": "Burundi", "value": "BI"}, + { "label": "Cambodia", "value": "KH"}, + { "label": "Cameroon", "value": "CM"}, + { "label": "Canada", "value": "CA"}, + { "label": "Cape Verde", "value": "CV"}, + { "label": "Cayman Islands", "value": "KY"}, + { "label": "Central African Republic", "value": "CF"}, + { "label": "Chad", "value": "TD"}, + { "label": "Chile", "value": "CL"}, + { "label": "China", "value": "CN"}, + { "label": "Christmas Island Australia", "value": "CX"}, + { "label": "Cocos Keeling Islands", "value": "CC"}, + { "label": "Colombia", "value": "CO"}, + { "label": "Comoros", "value": "KM"}, + { "label": "Congo", "value": "CG"}, + { "label": "Congo, D.R.", "value": "CD"}, + { "label": "Cook Islands", "value": "CK"}, + { "label": "Costa Rica", "value": "CR"}, + { "label": "Cote D'Ivoire Ivory Coast", "value": "CI"}, + { "label": "Croatia Hrvatska", "value": "HR"}, + { "label": "Cuba", "value": "CU"}, + { "label": "Cyprus", "value": "CY"}, + { "label": "Czech Republic", "value": "CZ"}, + { "label": "Denmark", "value": "DK"}, + { "label": "Djibouti", "value": "DJ"}, + { "label": "Dominica", "value": "DM"}, + { "label": "Dominican Republic", "value": "DO"}, + { "label": "Ecuador", "value": "EC"}, + { "label": "Egypt", "value": "EG"}, + { "label": "El Salvador", "value": "SV"}, + { "label": "Equatorial Guinea", "value": "GQ"}, + { "label": "Eritrea", "value": "ER"}, + { "label": "Estonia", "value": "EE"}, + { "label": "Ethiopia", "value": "ET"}, + { "label": "Faeroe Islands", "value": "FO"}, + { "label": "Falkland Islands Malvinas", "value": "FK"}, + { "label": "Fiji", "value": "FJ"}, + { "label": "Finland", "value": "FI"}, + { "label": "France", "value": "FR"}, + { "label": "France, Metropolitan", "value": "FX"}, + { "label": "French Guiana", "value": "GF"}, + { "label": "French Polynesia", "value": "PF"}, + { "label": "French Southern Territories", "value": "TF"}, + { "label": "Gabon", "value": "GA"}, + { "label": "Gambia", "value": "GM"}, + { "label": "Georgia", "value": "GE"}, + { "label": "Germany", "value": "DE"}, + { "label": "Ghana", "value": "GH"}, + { "label": "Gibraltar", "value": "GI"}, + { "label": "Greece", "value": "GR"}, + { "label": "Greenland", "value": "GL"}, + { "label": "Grenada", "value": "GD"}, + { "label": "Guadeloupe", "value": "GP"}, + { "label": "Guam", "value": "GU"}, + { "label": "Guatemala", "value": "GT"}, + { "label": "Guinea", "value": "GN"}, + { "label": "Guinea Bissau", "value": "GW"}, + { "label": "Guyana", "value": "GY"}, + { "label": "Haiti", "value": "HT"}, + { "label": "Heard and McDonald Is.", "value": "HM"}, + { "label": "Honduras", "value": "HN"}, + { "label": "Hong Kong", "value": "HK"}, + { "label": "Hungary", "value": "HU"}, + { "label": "Iceland", "value": "IS"}, + { "label": "India", "value": "IN"}, + { "label": "Indonesia", "value": "ID"}, + { "label": "Iran", "value": "IR"}, + { "label": "Iraq", "value": "IQ"}, + { "label": "Isle of Man", "value": "IM"}, + { "label": "Ireland", "value": "IE"}, + { "label": "Israel", "value": "IL"}, + { "label": "Italy", "value": "IT"}, + { "label": "Jamaica", "value": "JM"}, + { "label": "Japan", "value": "JP"}, + { "label": "Jersey", "value": "JE"}, + { "label": "Jordan", "value": "JO"}, + { "label": "Kazakhstan", "value": "KZ"}, + { "label": "Kenya", "value": "KE"}, + { "label": "Kiribati", "value": "KI"}, + { "label": "Korea North", "value": "KP"}, + { "label": "Korea South", "value": "KR"}, + { "label": "Kuwait", "value": "KW"}, + { "label": "Kyrgyzstan", "value": "KG"}, + { "label": "Lao P.Dem.R.", "value": "LA"}, + { "label": "Latvia", "value": "LV"}, + { "label": "Lebanon", "value": "LB"}, + { "label": "Lesotho", "value": "LS"}, + { "label": "Liberia", "value": "LR"}, + { "label": "Libyan Arab Jamahiriya", "value": "LY"}, + { "label": "Liechtenstein", "value": "LI"}, + { "label": "Lithuania", "value": "LT"}, + { "label": "Luxembourg", "value": "LU"}, + { "label": "Macau", "value": "MO"}, + { "label": "Macedonia", "value": "MK"}, + { "label": "Madagascar", "value": "MG"}, + { "label": "Malawi", "value": "MW"}, + { "label": "Malaysia", "value": "MY"}, + { "label": "Maldives", "value": "MV"}, + { "label": "Mali", "value": "ML"}, + { "label": "Malta", "value": "MT"}, + { "label": "Marshall Islands", "value": "MH"}, + { "label": "Martinique", "value": "MQ"}, + { "label": "Mauritania", "value": "MR"}, + { "label": "Mauritius", "value": "MU"}, + { "label": "Mayotte", "value": "YT"}, + { "label": "Mexico", "value": "MX"}, + { "label": "Micronesia", "value": "FM"}, + { "label": "Moldova", "value": "MD"}, + { "label": "Monaco", "value": "MC"}, + { "label": "Mongolia", "value": "MN"}, + { "label": "Montenegro", "value": "ME"}, + { "label": "Montserrat", "value": "MS"}, + { "label": "Morocco", "value": "MA"}, + { "label": "Mozambique", "value": "MZ"}, + { "label": "Myanmar", "value": "MM"}, + { "label": "Namibia", "value": "NA"}, + { "label": "Nauru", "value": "NR"}, + { "label": "Nepal", "value": "NP"}, + { "label": "Netherlands", "value": "NL"}, + { "label": "Netherlands Antilles", "value": "AN"}, + { "label": "New Caledonia", "value": "NC"}, + { "label": "New Zealand", "value": "NZ"}, + { "label": "Nicaragua", "value": "NI"}, + { "label": "Niger", "value": "NE"}, + { "label": "Nigeria", "value": "NG"}, + { "label": "Niue", "value": "NU"}, + { "label": "Norfolk Island", "value": "NF"}, + { "label": "Northern Mariana Islands", "value": "MP"}, + { "label": "Norway", "value": "NO"}, + { "label": "Oman", "value": "OM"}, + { "label": "Pakistan", "value": "PK"}, + { "label": "Palau", "value": "PW"}, + { "label": "Palestinian Territory, Occupied", "value": "PS"}, + { "label": "Panama", "value": "PA"}, + { "label": "Papua New Guinea", "value": "PG"}, + { "label": "Paraguay", "value": "PY"}, + { "label": "Peru", "value": "PE"}, + { "label": "Philippines", "value": "PH"}, + { "label": "Pitcairn", "value": "PN"}, + { "label": "Poland", "value": "PL"}, + { "label": "Portugal", "value": "PT"}, + { "label": "Puerto Rico", "value": "PR"}, + { "label": "Qatar", "value": "QA"}, + { "label": "Reunion", "value": "RE"}, + { "label": "Romania", "value": "RO"}, + { "label": "Russian Federation", "value": "RU"}, + { "label": "Rwanda", "value": "RW"}, + { "label": "Saint Helena", "value": "SH"}, + { "label": "Saint Kitts and Nevis", "value": "KN"}, + { "label": "Saint Lucia", "value": "LC"}, + { "label": "Saint Pierre and Miquelon", "value": "PM"}, + { "label": "Saint Vincent and the Grenadines", "value": "VC"}, + { "label": "Samoa", "value": "WS"}, + { "label": "San Marino", "value": "SM"}, + { "label": "Sao Tome and Principe", "value": "ST"}, + { "label": "Saudi Arabia", "value": "SA"}, + { "label": "Senegal", "value": "SN"}, + { "label": "Serbia", "value": "RS"}, + { "label": "Seychelles", "value": "SC"}, + { "label": "Sierra Leone", "value": "SL"}, + { "label": "Singapore", "value": "SG"}, + { "label": "Slovakia", "value": "SK"}, + { "label": "Slovenia", "value": "SI"}, + { "label": "Solomon Islands", "value": "SB"}, + { "label": "Somalia", "value": "SO"}, + { "label": "South Africa", "value": "ZA"}, + { "label": "S. Georgia & S. Sandwich Is.", "value": "GS"}, + { "label": "Spain", "value": "ES"}, + { "label": "Sri Lanka", "value": "LK"}, + { "label": "Sudan", "value": "SD"}, + { "label": "Suriname", "value": "SR"}, + { "label": "Svalbard & Jan Mayen Is.", "value": "SJ"}, + { "label": "Swaziland", "value": "SZ"}, + { "label": "Sweden", "value": "SE"}, + { "label": "Switzerland", "value": "CH"}, + { "label": "Syrian Arab Rep.", "value": "SY"}, + { "label": "Taiwan", "value": "TW"}, + { "label": "Tajikistan", "value": "TJ"}, + { "label": "Tanzania", "value": "TZ"}, + { "label": "Thailand", "value": "TH"}, + { "label": "Timor-Leste", "value": "TG"}, + { "label": "Togo", "value": "TG"}, + { "label": "Tokelau", "value": "TK"}, + { "label": "Tonga", "value": "TO"}, + { "label": "Trinidad and Tobago", "value": "TT"}, + { "label": "Tunisia", "value": "TN"}, + { "label": "Turkey", "value": "TR"}, + { "label": "Turkmenistan", "value": "TM"}, + { "label": "Turks and Caicos Islands", "value": "TC"}, + { "label": "Tuvalu", "value": "TU"}, + { "label": "Uganda", "value": "UG"}, + { "label": "Ukraine", "value": "UA"}, + { "label": "United Kingdom", "value": "GB"}, + { "label": "United Arab Emirates", "value": "AE"}, + { "label": "US Minor Outlying Is.", "value": "UM"}, + { "label": "Uruguay", "value": "UY"}, + { "label": "Uzbekistan", "value": "UZ"}, + { "label": "Vanuatu", "value": "VU"}, + { "label": "Vatican City State", "value": "VC"}, + { "label": "Venezuela", "value": "VE"}, + { "label": "Viet Nam", "value": "VN"}, + { "label": "Virgin Islands British", "value": "VG"}, + { "label": "Virgin Islands US", "value": "VI"}, + { "label": "Wallis and Futuna Islnds", "value": "WF"}, + { "label": "Western Sahara", "value": "EH"}, + { "label": "Yemen", "value": "YE"}, + { "label": "Yugoslavia", "value": "YU"}, + { "label": "Zambia", "value": "ZM"}, + { "label": "Zimbabwe", "value": "ZW"} + ]; + var _states = [ + { "label": "Alabama", "value": "AL", "country": "US" }, + { "label": "Alaska", "value": "AK", "country": "US" }, + { "label": "Arizona", "value": "AZ", "country": "US" }, + { "label": "Arkansas", "value": "AR", "country": "US" }, + { "label": "California", "value": "CA", "country": "US" }, + { "label": "Colorado", "value": "CO", "country": "US" }, + { "label": "Connecticut", "value": "CT", "country": "US" }, + { "label": "Delaware", "value": "DE", "country": "US" }, + { "label": "District of Columbia", "value": "DC", "country": "US" }, + { "label": "Florida", "value": "FL", "country": "US" }, + { "label": "Georgia", "value": "GA", "country": "US" }, + { "label": "Hawaii", "value": "HI", "country": "US" }, + { "label": "Idaho", "value": "ID", "country": "US" }, + { "label": "Illinois", "value": "IL", "country": "US" }, + { "label": "Indiana", "value": "IN", "country": "US" }, + { "label": "Iowa", "value": "IA", "country": "US" }, + { "label": "Kansas", "value": "KS", "country": "US" }, + { "label": "Kentucky", "value": "KY", "country": "US" }, + { "label": "Louisiana", "value": "LA", "country": "US" }, + { "label": "Maine", "value": "ME", "country": "US" }, + { "label": "Maryland", "value": "MD", "country": "US" }, + { "label": "Massachusetts", "value": "MA", "country": "US" }, + { "label": "Michigan", "value": "MI", "country": "US" }, + { "label": "Minnesota", "value": "MN", "country": "US" }, + { "label": "Mississippi", "value": "MS", "country": "US" }, + { "label": "Missouri", "value": "MO", "country": "US" }, + { "label": "Montana", "value": "MT", "country": "US" }, + { "label": "Nebraska", "value": "NE", "country": "US" }, + { "label": "Nevada", "value": "NV", "country": "US" }, + { "label": "New Hampshire", "value": "NH", "country": "US" }, + { "label": "New Jersey", "value": "NJ", "country": "US" }, + { "label": "New Mexico", "value": "NM", "country": "US" }, + { "label": "New York", "value": "NY", "country": "US" }, + { "label": "North Carolina", "value": "NC", "country": "US" }, + { "label": "North Dakota", "value": "ND", "country": "US" }, + { "label": "Ohio", "value": "OH", "country": "US" }, + { "label": "Oklahoma", "value": "OK", "country": "US" }, + { "label": "Oregon", "value": "OR", "country": "US" }, + { "label": "Pennsylvania", "value": "PA", "country": "US" }, + { "label": "Rhode Island", "value": "RI", "country": "US" }, + { "label": "South Carolina", "value": "SC", "country": "US" }, + { "label": "South Dakota", "value": "SD", "country": "US" }, + { "label": "Tennessee", "value": "TN", "country": "US" }, + { "label": "Texas", "value": "TX", "country": "US" }, + { "label": "Utah", "value": "UT", "country": "US" }, + { "label": "Vermont", "value": "VT", "country": "US" }, + { "label": "Virginia", "value": "VA", "country": "US" }, + { "label": "Washington", "value": "WA", "country": "US" }, + { "label": "West Virginia", "value": "WV", "country": "US" }, + { "label": "Wisconsin", "value": "WI", "country": "US" }, + { "label": "Wyoming", "value": "WY", "country": "US" }, + { "label": "Armed Forces Americas (AA)", "value": "AA", "country": "US" }, + { "label": "Armed Forces Africa/Canada/Europe/Middle East (AE)", "value": "AE", "country": "US" }, + { "label": "Armed Forces Pacific (AP)", "value": "AP", "country": "US" }, + { "label": "American Samoa", "value": "AS", "country": "US" }, + { "label": "Federated States of Micronesia", "value": "FM", "country": "US" }, + { "label": "Guam", "value": "GU", "country": "US" }, + { "label": "Marshall Islands", "value": "MH", "country": "US" }, + { "label": "Northern Mariana Islands", "value": "MP", "country": "US" }, + { "label": "Palau", "value": "PW", "country": "US" }, + { "label": "Puerto Rico", "value": "PR", "country": "US" }, + { "label": "Virgin Islands", "value": "VI", "country": "US" }, + { "label": "Drenthe", "value": "Drenthe", "country": "NL" }, + { "label": "Flevoland", "value": "Flevoland", "country": "NL" }, + { "label": "Friesland", "value": "Friesland", "country": "NL" }, + { "label": "Gelderland", "value": "Gelderland", "country": "NL" }, + { "label": "Groningen", "value": "Groningen", "country": "NL" }, + { "label": "Limburg", "value": "Limburg", "country": "NL" }, + { "label": "Noord-Brabant", "value": "Noord-Brabant", "country": "NL" }, + { "label": "Noord-Holland", "value": "Noord-Holland", "country": "NL" }, + { "label": "Overijssel", "value": "Overijssel", "country": "NL" }, + { "label": "Utrecht", "value": "Utrecht", "country": "NL" }, + { "label": "Zeeland", "value": "Zeeland", "country": "NL" }, + { "label": "Zuid-Holland", "value": "Zuid-Holland", "country": "NL" }, + { "label": "Alberta", "value": "AB", "country": "CA" }, + { "label": "British Columbia", "value": "BC", "country": "CA" }, + { "label": "Manitoba", "value": "MB", "country": "CA" }, + { "label": "New Brunswick", "value": "NB", "country": "CA" }, + { "label": "Newfoundland and Labrador", "value": "NL", "country": "CA" }, + { "label": "Northwest Territories", "value": "NT", "country": "CA" }, + { "label": "Nova Scotia", "value": "NS", "country": "CA" }, + { "label": "Nunavut", "value": "NU", "country": "CA" }, + { "label": "Ontario", "value": "ON", "country": "CA" }, + { "label": "Prince Edward Island", "value": "PE", "country": "CA" }, + { "label": "Quebec", "value": "QC", "country": "CA" }, + { "label": "Saskatchewan", "value": "SK", "country": "CA" }, + { "label": "Yukon", "value": "YT", "country": "CA" } + ]; + + return { + Countries: _countries, + States: _states + }; +} \ No newline at end of file diff --git a/src/app/common/services/oc-lineitems.js b/src/app/common/services/oc-lineitems.js new file mode 100644 index 00000000..a4ea15f3 --- /dev/null +++ b/src/app/common/services/oc-lineitems.js @@ -0,0 +1,137 @@ +angular.module('orderCloud') + .factory('ocLineItems', LineItemFactory) +; + +function LineItemFactory($rootScope, $q, $uibModal, OrderCloud) { + return { + SpecConvert: _specConvert, + AddItem: _addItem, + GetProductInfo: _getProductInfo, + CustomShipping: _customShipping, + UpdateShipping: _updateShipping, + ListAll: _listAll + }; + + function _specConvert(specs) { + var results = []; + angular.forEach(specs, function (spec) { + var spec_to_push = {SpecID: spec.ID}; + if (spec.Options.length > 0) { + if (spec.DefaultOptionID) { + spec_to_push.OptionID = spec.DefaultOptionID; + } + if (spec.OptionID) { + spec_to_push.OptionID = spec.OptionID; + } + if (spec.Value) { + spec_to_push.Value = spec.Value; + } + } + else { + spec_to_push.Value = spec.Value || spec.DefaultValue || null; + } + results.push(spec_to_push); + }); + return results; + } + + function _addItem(order, product){ + var deferred = $q.defer(); + + var li = { + ProductID: product.ID, + Quantity: product.Quantity, + Specs: _specConvert(product.Specs) + }; + li.ShippingAddressID = isSingleShipping(order) ? getSingleShippingAddressID(order) : null; + OrderCloud.LineItems.Create(order.ID, li) + .then(function(lineItem) { + $rootScope.$broadcast('OC:UpdateOrder', order.ID); + deferred.resolve(); + }) + .catch(function(error) { + deferred.reject(error); + }); + + function isSingleShipping(order) { + return _.pluck(order.LineItems, 'ShippingAddressID').length == 1; + } + + function getSingleShippingAddressID(order) { + return order.LineItems[0].ShippingAddressID; + } + + return deferred.promise; + } + + function _getProductInfo(LineItems) { + var li = LineItems.Items || LineItems; + var productIDs = _.uniq(_.pluck(li, 'ProductID')); + var dfd = $q.defer(); + var queue = []; + angular.forEach(productIDs, function (productid) { + queue.push(OrderCloud.Me.GetProduct(productid)); + }); + $q.all(queue) + .then(function (results) { + angular.forEach(li, function (item) { + item.Product = angular.copy(_.where(results, {ID: item.ProductID})[0]); + }); + dfd.resolve(li); + }); + return dfd.promise; + } + + function _customShipping(Order, LineItem) { + var modalInstance = $uibModal.open({ + animation: true, + templateUrl: 'common/lineitems/templates/shipping.tpl.html', + controller: 'LineItemModalCtrl', + controllerAs: 'liModal', + size: 'lg' + }); + + modalInstance.result + .then(function (address) { + address.ID = Math.floor(Math.random() * 1000000).toString(); + OrderCloud.LineItems.SetShippingAddress(Order.ID, LineItem.ID, address) + .then(function () { + $rootScope.$broadcast('LineItemAddressUpdated', LineItem.ID, address); + }); + }); + } + + function _updateShipping(Order, LineItem, AddressID) { + OrderCloud.Addresses.Get(AddressID) + .then(function (address) { + OrderCloud.LineItems.SetShippingAddress(Order.ID, LineItem.ID, address); + $rootScope.$broadcast('LineItemAddressUpdated', LineItem.ID, address); + }); + } + + function _listAll(orderID) { + var li; + var dfd = $q.defer(); + var queue = []; + OrderCloud.LineItems.List(orderID, null, 1, 100) + .then(function (data) { + li = data; + if (data.Meta.TotalPages > data.Meta.Page) { + var page = data.Meta.Page; + while (page < data.Meta.TotalPages) { + page += 1; + queue.push(OrderCloud.LineItems.List(orderID, null, page, 100)); + } + } + $q.all(queue) + .then(function (results) { + angular.forEach(results, function (result) { + li.Items = [].concat(li.Items, result.Items); + li.Meta = result.Meta; + }); + dfd.resolve(li.Items); + }); + }); + return dfd.promise; + } +} \ No newline at end of file diff --git a/src/app/common/services/oc-media.js b/src/app/common/services/oc-media.js new file mode 100644 index 00000000..8e6fd1a3 --- /dev/null +++ b/src/app/common/services/oc-media.js @@ -0,0 +1,122 @@ +angular.module('orderCloud') + .factory('$ocMedia', ocMediaFactory) + .constant('MEDIA', MEDIA_CONSTANT) + .constant('MEDIA_PRIORITY', MEDIA_PRIORITY_CONSTANT) +; + +function MEDIA_CONSTANT() { + return { + 'sm': '(max-width: 600px)', + 'gt-sm': '(min-width: 600px)', + 'md': '(min-width: 600px) and (max-width: 960px)', + 'gt-md': '(min-width: 960px)', + 'lg': '(min-width: 960px) and (max-width: 1200px)', + 'gt-lg': '(min-width: 1200px)' + } +} + +function MEDIA_PRIORITY_CONSTANT() { + return [ + 'gt-lg', + 'lg', + 'gt-md', + 'md', + 'gt-sm', + 'sm' + ]; +} + +function ocMediaFactory(MEDIA, MEDIA_PRIORITY, $rootScope, $window) { + var queries = {}; + var mqls = {}; + var results = {}; + var normalizeCache = {}; + + $ocMedia.getResponsiveAttribute = getResponsiveAttribute; + $ocMedia.getQuery = getQuery; + $ocMedia.watchResponsiveAttributes = watchResponsiveAttributes; + + return $ocMedia; + + function $ocMedia(query) { + var validated = queries[query]; + if (angular.isUndefined(validated)) { + validated = queries[query] = validate(query); + } + + var result = results[validated]; + if (angular.isUndefined(result)) { + result = add(validated); + } + + return result; + } + + function validate(query) { + return MEDIA[query] || + ((query.charAt(0) !== '(') ? ('(' + query + ')') : query); + } + + function add(query) { + var result = mqls[query] = $window.matchMedia(query); + result.addListener(onQueryChange); + return (results[result.media] = !!result.matches); + } + + function onQueryChange(query) { + $rootScope.$evalAsync(function() { + results[query.media] = !!query.matches; + }); + } + + function getQuery(name) { + return mqls[name]; + } + + function getResponsiveAttribute(attrs, attrName) { + for (var i = 0; i < MEDIA_PRIORITY.length; i++) { + var mediaName = MEDIA_PRIORITY[i]; + if (!mqls[queries[mediaName]].matches) { + continue; + } + + var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); + if (attrs[normalizedName]) { + return attrs[normalizedName]; + } + } + + // fallback on unprefixed + return attrs[getNormalizedName(attrs, attrName)]; + } + + function watchResponsiveAttributes(attrNames, attrs, watchFn) { + var unwatchFns = []; + attrNames.forEach(function(attrName) { + var normalizedName = getNormalizedName(attrs, attrName); + if (attrs[normalizedName]) { + unwatchFns.push( + attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null))); + } + + for (var mediaName in MEDIA) { + normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); + if (!attrs[normalizedName]) { + return; + } + + unwatchFns.push(attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName))); + } + }); + + return function unwatch() { + unwatchFns.forEach(function(fn) { fn(); }) + }; + } + + // Improves performance dramatically + function getNormalizedName(attrs, attrName) { + return normalizeCache[attrName] || + (normalizeCache[attrName] = attrs.$normalize(attrName)); + } +} diff --git a/src/app/common/services/oc-parameters.js b/src/app/common/services/oc-parameters.js new file mode 100644 index 00000000..17eab7e3 --- /dev/null +++ b/src/app/common/services/oc-parameters.js @@ -0,0 +1,49 @@ +angular.module('orderCloud') + .factory('ocParameters', OrderCloudParametersService) +; + +function OrderCloudParametersService() { + var service = { + Get: _get, //get params for use in OrderCloud service + Create: _create //create params obj ready for use in OrderCloud $state.go() + }; + + function _get(stateParams, suffix) { + var parameters = angular.copy(stateParams); + var suffixParams; + parameters.filters = parameters.filters ? JSON.parse(parameters.filters) : null; + parameters.from ? parameters.from = new Date(parameters.from) : angular.noop(); //Translate date string to date obj + parameters.to ? parameters.to = new Date(parameters.to) : angular.noop(); //Translate date string to date obj + if (suffix) { + suffixParams = {}; + angular.forEach(parameters, function(val, key) { + suffixParams[key.split(suffix)[0]] = val; + }); + } + return suffixParams || parameters; + } + + function _create(params, resetPage, suffix) { + var parameters = angular.copy(params); + var suffixParams; + resetPage ? parameters.page = null : angular.noop(); //Reset page when filters are applied + if (parameters.filters) { + parameters.filters.orderType == '' ? delete parameters.filters.orderType : angular.noop(); + parameters.filters.type == '' ? delete parameters.filters.type : angular.noop(); + parameters.filters.status == '' ? delete parameters.filters.status : angular.noop(); + parameters.filters = JSON.stringify(parameters.filters); //Translate filter object to string + parameters.filters == '{}' ? parameters.filters = null : angular.noop(); //Null out the filter string if it's an empty obj + } + parameters.from ? parameters.from = parameters.from.toISOString() : angular.noop(); + parameters.to ? parameters.to = parameters.to.toISOString() : angular.noop(); + if (suffix) { + suffixParams = {}; + angular.forEach(parameters, function(val, key) { + suffixParams[key + suffix] = val; + }); + } + return suffixParams || parameters; + } + + return service; +} \ No newline at end of file diff --git a/src/app/common/templates/card.product.tpl.html b/src/app/common/templates/card.product.tpl.html new file mode 100644 index 00000000..c32eb9d0 --- /dev/null +++ b/src/app/common/templates/card.product.tpl.html @@ -0,0 +1,17 @@ +
+ +
+
+ {{product.ID}} + {{product.StandardPriceSchedule.PriceBreaks[0].Price | currency}} x {{product.StandardPriceSchedule.PriceBreaks[0].Quantity}} +
+

{{product.Name || product.ID}}

+
+

{{product.Description}}

+
+
+ + +
+
+
\ No newline at end of file diff --git a/src/app/common/templates/confirm.modal.tpl.html b/src/app/common/templates/confirm.modal.tpl.html new file mode 100644 index 00000000..325c1d7b --- /dev/null +++ b/src/app/common/templates/confirm.modal.tpl.html @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/src/app/common/templates/quantityInput.tpl.html b/src/app/common/templates/quantityInput.tpl.html new file mode 100644 index 00000000..e6cefd8d --- /dev/null +++ b/src/app/common/templates/quantityInput.tpl.html @@ -0,0 +1,55 @@ +
+
+ + + + + {{'Quantity multiplier x ' + item.QuantityMultiplier + (item.Quantity ? (' (' + (item.Quantity * item.QuantityMultiplier) + ')') : '')}} + +
+
+ + + + + {{'x ' + item.Product.QuantityMultiplier + (item.Quantity ? (' (' + (item.Quantity * item.Product.QuantityMultiplier) + ')') : '')}} + +
+
+ + + + diff --git a/src/app/common/templates/view.loading.tpl.html b/src/app/common/templates/view.loading.tpl.html new file mode 100644 index 00000000..9203bf46 --- /dev/null +++ b/src/app/common/templates/view.loading.tpl.html @@ -0,0 +1,9 @@ +
+
+
+
+
+
+
+
+ diff --git a/src/app/common/tests/oc-lineitems.spec.js b/src/app/common/tests/oc-lineitems.spec.js new file mode 100644 index 00000000..e2bebec7 --- /dev/null +++ b/src/app/common/tests/oc-lineitems.spec.js @@ -0,0 +1,42 @@ +describe('Factory: ocLineItem', function() { + var scope, + q, + oc, + _ocLineItems, + order = { + ID: "FAKE_ORDER_ID", + LineItems: [] + }, + product = { + ID: "FAKE_PRODUCT_ID", + Quantity: 2 + }; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($rootScope, $q, OrderCloud, ocLineItems) { + scope = $rootScope.$new(); + q = $q; + oc = OrderCloud; + _ocLineItems = ocLineItems; + })); + describe('AddItem', function() { + it ('should start up a new $q.defer()', function() { + spyOn(q, 'defer').and.callThrough(); + _ocLineItems.AddItem(order, product); + expect(q.defer).toHaveBeenCalled(); + }); + it ('should call OrderCloud.LineItems.Create()', function() { + var defer = q.defer(); + defer.resolve("NEW_LINE_ITEM"); + spyOn(oc.LineItems, 'Create').and.returnValue(defer.promise); + + _ocLineItems.AddItem(order, product); + expect(oc.LineItems.Create).toHaveBeenCalledWith(order.ID, { + ProductID: product.ID, + Quantity: product.Quantity, + Specs: [], + ShippingAddressID: null + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/favoriteOrders/favoriteOrder.js b/src/app/favoriteOrders/favoriteOrder.js new file mode 100644 index 00000000..011d526b --- /dev/null +++ b/src/app/favoriteOrders/favoriteOrder.js @@ -0,0 +1,46 @@ +angular.module('orderCloud') + .component('ordercloudFavoriteOrder', { + bindings:{ + currentUser: '<', + order: '<' + }, + templateUrl: 'favoriteOrders/templates/favoriteOrder.component.tpl.html', + controller: FavoriteOrderCtrl + }); + +function FavoriteOrderCtrl(OrderCloud, toastr){ + var vm = this; + vm.$onInit = function(){ + vm.hasFavorites = !!vm.currentUser && !!vm.currentUser.xp && !!vm.currentUser.xp.FavoriteOrders; + vm.isFavorited = !!vm.hasFavorites && vm.currentUser.xp.FavoriteOrders.indexOf(vm.order.ID) > -1; + }; + + vm.toggleFavoriteOrder = function(){ + if (vm.hasFavorites && vm.isFavorited){ + removeOrder(); + } else if (vm.hasFavorites && !vm.isFavorited) { + addOrder(vm.currentUser.xp.FavoriteOrders); + } else { + addOrder([]); + } + }; + + function addOrder(existingList) { + existingList.push(vm.order.ID); + OrderCloud.Me.Patch({xp: {FavoriteOrders: existingList}}) + .then(function(){ + vm.isFavorited = true; + toastr.success('Order added to your favorites', 'Success'); + }); + } + + function removeOrder(){ + var updatedList = _.without(vm.currentUser.xp.FavoriteOrders, vm.order.ID); + OrderCloud.Me.Patch({xp: {FavoriteOrders: updatedList}}) + .then(function(){ + vm.isFavorited = false; + vm.currentUser.xp.FavoriteOrders = updatedList; + toastr.success('Order removed from your favorites', 'Success'); + }); + } +} \ No newline at end of file diff --git a/src/app/favoriteOrders/templates/favoriteOrder.component.tpl.html b/src/app/favoriteOrders/templates/favoriteOrder.component.tpl.html new file mode 100644 index 00000000..6659752e --- /dev/null +++ b/src/app/favoriteOrders/templates/favoriteOrder.component.tpl.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/favoriteOrders/tests/favoriteOrder.spec.js b/src/app/favoriteOrders/tests/favoriteOrder.spec.js new file mode 100644 index 00000000..2d7d3ecf --- /dev/null +++ b/src/app/favoriteOrders/tests/favoriteOrder.spec.js @@ -0,0 +1,69 @@ +describe('Component: FavoriteOrders', function(){ + var q, + oc, + scope, + toaster + ; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($q, OrderCloud, $rootScope, toastr){ + q = $q; + oc = OrderCloud; + scope = $rootScope.$new(); + toaster = toastr; + })); + describe('Controller: FavoriteOrderCtrl', function(){ + var favoriteOrderCtrl; + beforeEach(inject(function($componentController){ + favoriteOrderCtrl = $componentController('ordercloudFavoriteOrder', { + $scope: scope, + OrderCloud: oc, + toastr: toaster + }); + + })); + describe('toggleFavoriteOrder', function(){ + var mockOrderID, + mockFavoriteOrder + ; + beforeEach(function(){ + mockOrderID = 'OrderID123'; + favoriteOrderCtrl.order = {ID: mockOrderID}; + mockFavoriteOrder = 'FavoriteOrder1'; + favoriteOrderCtrl.currentUser = {xp: {FavoriteOrders: [mockFavoriteOrder]}}; + + var defer = q.defer(); + defer.resolve(); + spyOn(oc.Me, 'Patch').and.returnValue(defer.promise); + spyOn(toaster, 'success'); + }); + it('should add to favorites if user doesnt have any favorite orders', function(){ + favoriteOrderCtrl.hasFavorites = false; + favoriteOrderCtrl.toggleFavoriteOrder(); + expect(oc.Me.Patch).toHaveBeenCalledWith({xp: {FavoriteOrders: [mockOrderID]}}); + scope.$digest(); + expect(favoriteOrderCtrl.isFavorited).toBe(true); + expect(toaster.success).toHaveBeenCalledWith('Order added to your favorites', 'Success'); + }); + it('should add to favorites if user has favorites list, but the order isnt included in the list', function(){ + favoriteOrderCtrl.hasFavorites = true; + favoriteOrderCtrl.isFavorited = false; + favoriteOrderCtrl.toggleFavoriteOrder(); + expect(oc.Me.Patch).toHaveBeenCalledWith({xp: {FavoriteOrders: [mockFavoriteOrder, mockOrderID]}}); + scope.$digest(); + expect(favoriteOrderCtrl.isFavorited).toBe(true); + expect(toaster.success).toHaveBeenCalledWith('Order added to your favorites', 'Success'); + }); + it('should remove order from favorite list, if order is already on list', function(){ + favoriteOrderCtrl.hasFavorites = true; + favoriteOrderCtrl.isFavorited = true; + favoriteOrderCtrl.order = {ID: mockFavoriteOrder}; + favoriteOrderCtrl.toggleFavoriteOrder(); + expect(oc.Me.Patch).toHaveBeenCalledWith({xp: {FavoriteOrders: [ ]}}); + scope.$digest(); + expect(favoriteOrderCtrl.isFavorited).toBe(false); + expect(toaster.success).toHaveBeenCalledWith('Order removed from your favorites', 'Success'); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/favoriteProducts/favoriteProducts.js b/src/app/favoriteProducts/favoriteProducts.js new file mode 100644 index 00000000..69cb7225 --- /dev/null +++ b/src/app/favoriteProducts/favoriteProducts.js @@ -0,0 +1,145 @@ +angular.module('orderCloud') + .config(FavoriteProductsConfig) + .directive('ordercloudFavoriteProduct', FavoriteProductDirective) + .controller('FavoriteProductsCtrl', FavoriteProductsController) + .controller('FavoriteProductCtrl', FavoriteProductController) +; + +function FavoriteProductsConfig($stateProvider){ + $stateProvider + .state('favoriteProducts', { + parent: 'account', + templateUrl: 'favoriteProducts/templates/favoriteProducts.tpl.html', + url: '/favorite-products?search?page?pageSize?searchOn?sortBy?filters?depth', + controller: 'FavoriteProductsCtrl', + controllerAs: 'favoriteProducts', + data: { + pageTitle: "Favorite Products" + }, + resolve: { + Parameters: function ($stateParams, ocParameters) { + return ocParameters.Get($stateParams); + }, + FavoriteProducts: function(OrderCloud, Parameters, CurrentUser){ + if (CurrentUser.xp && CurrentUser.xp.FavoriteProducts.length) { + return OrderCloud.Me.ListProducts(Parameters.search, Parameters.page, Parameters.pageSize || 6, Parameters.searchOn, Parameters.sortBy, {ID: CurrentUser.xp.FavoriteProducts.join('|')}); + } else { + return null; + } + } + } + }); +} + +function FavoriteProductsController(ocParameters, OrderCloud, $state, $ocMedia, Parameters, CurrentUser, FavoriteProducts){ + var vm = this; + vm.currentUser = CurrentUser; + vm.list = FavoriteProducts; + vm.parameters = Parameters; + + vm.sortSelection = Parameters.sortBy ? (Parameters.sortBy.indexOf('!') == 0 ? Parameters.sortBy.split('!')[1] : Parameters.sortBy) : null; + + //Filtering and Search Functionality + //check if filters are applied + vm.filtersApplied = vm.parameters.filters || ($ocMedia('max-width: 767px') && vm.sortSelection); + vm.showFilters = vm.filtersApplied; + + + //reload the state with new filters + vm.filter = function(resetPage) { + $state.go('.', ocParameters.Create(vm.parameters, resetPage)); + }; + + //clear the relevant filters, reload the state & reset the page + vm.clearFilters = function() { + vm.parameters.filters = null; + $ocMedia('max-width: 767px') ? vm.parameters.sortBy = null : angular.noop(); + vm.filter(true); + }; + + vm.updateSort = function(value) { + value ? angular.noop() : value = vm.sortSelection; + switch (vm.parameters.sortBy) { + case value: + vm.parameters.sortBy = '!' + value; + break; + case '!' + value: + vm.parameters.sortBy = null; + break; + default: + vm.parameters.sortBy = value; + } + vm.filter(false); + }; + + vm.reverseSort = function() { + Parameters.sortBy.indexOf('!') == 0 ? vm.parameters.sortBy = Parameters.sortBy.split('!')[1] : vm.parameters.sortBy = '!' + Parameters.sortBy; + vm.filter(false); + }; + + //reload the state with the incremented page parameter + vm.pageChanged = function() { + $state.go('.', { + page: vm.list.Meta.Page + }); + }; + + //load the next page of results with all the same parameters + vm.loadMore = function() { + return OrderCloud.Me.ListProducts(Parameters.search, vm.list.Meta.Page + 1, Parameters.pageSize || vm.list.Meta.PageSize, Parameters.searchOn, Parameters.sortBy, Parameters.filters) + .then(function(data) { + vm.list.Items = vm.list.Items.concat(data.Items); + vm.list.Meta = data.Meta; + }); + }; +} + +function FavoriteProductDirective(){ + return { + scope: { + currentUser: '=', + product: '=' + }, + restrict: 'E', + templateUrl: 'favoriteProducts/templates/ordercloud-favorite-product.tpl.html', + controller: 'FavoriteProductCtrl', + controllerAs: 'favoriteProduct' + }; +} + +function FavoriteProductController($scope, OrderCloud, toastr){ + var vm = this; + vm.hasFavorites = $scope.currentUser && $scope.currentUser.xp && $scope.currentUser.xp.FavoriteProducts; + vm.isFavorited = vm.hasFavorites && $scope.currentUser.xp.FavoriteProducts.indexOf($scope.product.ID) > -1; + + vm.toggleFavoriteProduct = function(){ + if (vm.hasFavorites){ + if (vm.isFavorited){ + removeProduct(); + } else { + addProduct($scope.currentUser.xp.FavoriteProducts); + } + + } else { + addProduct([]); + } + function addProduct(existingList){ + existingList.push($scope.product.ID); + OrderCloud.Me.Patch({xp: {FavoriteProducts: existingList}}) + .then(function(){ + vm.isFavorited = true; + toastr.success($scope.product.Name + ' was added to your favorites'); + }); + } + function removeProduct(){ + var updatedList = _.without($scope.currentUser.xp.FavoriteProducts, $scope.product.ID); + OrderCloud.Me.Patch({xp: {FavoriteProducts: updatedList}}) + .then(function(){ + vm.isFavorited = false; + $scope.currentUser.xp.FavoriteProducts = updatedList; + toastr.success($scope.product.Name + ' was removed from your favorites'); + }); + } + }; +} + diff --git a/src/app/favoriteProducts/templates/favoriteProducts.tpl.html b/src/app/favoriteProducts/templates/favoriteProducts.tpl.html new file mode 100644 index 00000000..e3f51175 --- /dev/null +++ b/src/app/favoriteProducts/templates/favoriteProducts.tpl.html @@ -0,0 +1,56 @@ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ {{(application.$ocMedia('min-width:768px') ? favoriteProducts.list.Meta.ItemRange[0] : '1') + ' - ' + favoriteProducts.list.Meta.ItemRange[1] + ' of ' + favoriteProducts.list.Meta.TotalCount + ' results'}} +
+
+
+
+ No matches found. +
+
+
+
+
+
+
+
+ +
+ +
\ No newline at end of file diff --git a/src/app/favoriteProducts/templates/ordercloud-favorite-product.tpl.html b/src/app/favoriteProducts/templates/ordercloud-favorite-product.tpl.html new file mode 100644 index 00000000..74548b95 --- /dev/null +++ b/src/app/favoriteProducts/templates/ordercloud-favorite-product.tpl.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/favoriteProducts/tests/favoriteProducts.spec.js b/src/app/favoriteProducts/tests/favoriteProducts.spec.js new file mode 100644 index 00000000..ad06db3b --- /dev/null +++ b/src/app/favoriteProducts/tests/favoriteProducts.spec.js @@ -0,0 +1,92 @@ +describe('Component: FavoriteProducts', function(){ + var q, + oc, + scope, + ocParams, + parameters, + favoriteProducts, + currentUser, + toaster, + product; + + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(module(function($provide) { + $provide.value('Parameters', {search:null, page: null, pageSize: null, searchOn: null, sortBy: null, userID: null, userGroupID: null, level: null, buyerID: null}); + $provide.value('FavoriteProducts', []); + $provide.value('CurrentUser', {xp: {FavoriteProducts: ['favoriteProduct']}}); + })); + beforeEach(inject(function($q, OrderCloud, $rootScope, Parameters, ocParameters, CurrentUser, FavoriteProducts, toastr){ + q = $q; + oc = OrderCloud; + scope = $rootScope.$new(); + parameters = Parameters; + ocParams = ocParameters; + favoriteProducts = FavoriteProducts; + currentUser = CurrentUser; + toaster = toastr; + product = { + ID: 'productID' + }; + })); + + describe('State: favoriteProducts', function(){ + var state; + beforeEach(inject(function($state){ + state = $state.get('favoriteProducts'); + var defer = q.defer(); + defer.resolve(); + spyOn(ocParams, 'Get').and.returnValue(null); + spyOn(oc.Me, 'ListProducts').and.returnValue(defer.promise); + })); + it('should resolve Parameters', inject(function($injector){ + $injector.invoke(state.resolve.Parameters); + expect(ocParams.Get).toHaveBeenCalled(); + })); + it('should resolve FavoriteProducts', inject(function(CurrentUser, $injector){ + $injector.invoke(state.resolve.FavoriteProducts); + currentUser.xp = {favoriteProducts: 'favoriteProduct'}; + expect(oc.Me.ListProducts).toHaveBeenCalledWith(parameters.search, parameters.page, parameters.pageSize || 6, parameters.searchOn, parameters.sortBy, {ID: currentUser.xp.favoriteProducts}); + })); + }); + describe('Controller: FavoriteProductCtrl', function(){ + var favoriteProductCtrl; + beforeEach(inject(function($state, $controller, CurrentUser){ + scope ={}; + scope.currentUser = CurrentUser; + scope.product = { + ID: 'productID' + }; + favoriteProductCtrl = $controller('FavoriteProductCtrl', { + $scope: scope, + OrderCloud: oc, + toastr: toaster + }); + + })); + describe('toggleFavoriteProduct', function(){ + beforeEach(function(){ + var defer = q.defer(); + defer.resolve(); + spyOn(oc.Me, 'Patch').and.returnValue(defer.promise); + spyOn(_, 'without').and.returnValue('updatedList'); + spyOn(toaster, 'success'); + }); + it('should call the Me Patch method when deleting a product', function(){ + var updatedList = 'updatedList'; + favoriteProductCtrl.hasFavorites = true; + favoriteProductCtrl.isFavorited = true; + favoriteProductCtrl.toggleFavoriteProduct(); + expect(_.without).toHaveBeenCalled(); + expect(oc.Me.Patch).toHaveBeenCalledWith({xp: {FavoriteProducts: updatedList}}); + }); + it('should call the Me Patch method when removing a product', function(){ + var existingList = ['favoriteProduct', 'productID']; + favoriteProductCtrl.hasFavorites = true; + favoriteProductCtrl.isFavorited = false; + favoriteProductCtrl.toggleFavoriteProduct(); + expect(oc.Me.Patch).toHaveBeenCalledWith({xp: {FavoriteProducts: existingList}}); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/home/home.js b/src/app/home/home.js index 157f600d..c5680f00 100644 --- a/src/app/home/home.js +++ b/src/app/home/home.js @@ -10,7 +10,14 @@ function HomeConfig($stateProvider) { url: '/home', templateUrl: 'home/templates/home.tpl.html', controller: 'HomeCtrl', +<<<<<<< HEAD controllerAs: 'home' +======= + controllerAs: 'home', + data: { + pageTitle: 'Home' + } +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }) ; } diff --git a/src/app/home/templates/home.tpl.html b/src/app/home/templates/home.tpl.html index 989a1292..49fa3fe8 100644 --- a/src/app/home/templates/home.tpl.html +++ b/src/app/home/templates/home.tpl.html @@ -1,3 +1,4 @@ +<<<<<<< HEAD
@@ -662,4 +663,12 @@

Media heading

+======= +
+
+

+

Welcome to the OrderCloud Marketplace App!

+

Learn more »

+
+>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2
\ No newline at end of file diff --git a/src/app/login/login.js b/src/app/login/login.js index 26c7d77e..948ace7a 100644 --- a/src/app/login/login.js +++ b/src/app/login/login.js @@ -2,6 +2,16 @@ angular.module('orderCloud') .config(LoginConfig) .factory('LoginService', LoginService) .controller('LoginCtrl', LoginController) +<<<<<<< HEAD +======= + .directive('prettySubmit', function () { + return function (scope, element) { + $(element).submit(function(event) { + event.preventDefault(); + }); + }; + }) +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 ; function LoginConfig($stateProvider) { @@ -15,11 +25,19 @@ function LoginConfig($stateProvider) { ; } +<<<<<<< HEAD function LoginService($q, $window, $state, toastr, OrderCloud, TokenRefresh, clientid, buyerid, anonymous) { +======= +function LoginService($q, $window, $state, $cookies, toastr, OrderCloud, clientid, buyerid, anonymous) { +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 return { SendVerificationCode: _sendVerificationCode, ResetPassword: _resetPassword, RememberMe: _rememberMe, +<<<<<<< HEAD +======= + AuthAnonymous: _authAnonymous, +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 Logout: _logout }; @@ -63,15 +81,32 @@ function LoginService($q, $window, $state, toastr, OrderCloud, TokenRefresh, cli return deferred.promise; } +<<<<<<< HEAD function _logout(){ OrderCloud.Auth.RemoveToken(); OrderCloud.Auth.RemoveImpersonationToken(); OrderCloud.BuyerID.Set(null); TokenRefresh.RemoveToken(); +======= + function _authAnonymous() { + return OrderCloud.Auth.GetToken('') + .then(function(data) { + OrderCloud.BuyerID.Set(buyerid); + OrderCloud.Auth.SetToken(data.access_token); + $state.go('home'); + }); + } + + function _logout() { + angular.forEach($cookies.getAll(), function(val, key) { + $cookies.remove(key); + }); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 $state.go(anonymous ? 'home' : 'login', {}, {reload: true}); } function _rememberMe() { +<<<<<<< HEAD TokenRefresh.GetToken() .then(function (refreshToken) { if (refreshToken) { @@ -92,6 +127,28 @@ function LoginService($q, $window, $state, toastr, OrderCloud, TokenRefresh, cli } function LoginController($state, $stateParams, $exceptionHandler, OrderCloud, LoginService, TokenRefresh, buyerid) { +======= + var availableRefreshToken = OrderCloud.Refresh.ReadToken() || null; + + if (availableRefreshToken) { + OrderCloud.Refresh.GetToken(availableRefreshToken) + .then(function(data) { + OrderCloud.BuyerID.Set(buyerid); + OrderCloud.Auth.SetToken(data.access_token); + $state.go('home'); + }) + .catch(function () { + toastr.error('Your token has expired, please log in again.'); + _logout(); + }); + } else { + _logout(); + } + } +} + +function LoginController($state, $stateParams, $exceptionHandler, OrderCloud, LoginService, buyerid) { +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 var vm = this; vm.credentials = { Username: null, @@ -105,9 +162,19 @@ function LoginController($state, $stateParams, $exceptionHandler, OrderCloud, Lo vm.rememberStatus = false; vm.submit = function() { +<<<<<<< HEAD OrderCloud.Auth.GetToken(vm.credentials) .then(function(data) { vm.rememberStatus ? TokenRefresh.SetToken(data['refresh_token']) : angular.noop(); +======= + $('#Username').blur(); + $('#Password').blur(); + $('#Remember').blur(); + $('#submit_login').blur(); + vm.loading = OrderCloud.Auth.GetToken(vm.credentials) + .then(function(data) { + vm.rememberStatus ? OrderCloud.Refresh.SetToken(data['refresh_token']) : angular.noop(); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 OrderCloud.BuyerID.Set(buyerid); OrderCloud.Auth.SetToken(data['access_token']); $state.go('home'); diff --git a/src/app/login/templates/login.tpl.html b/src/app/login/templates/login.tpl.html index 48d986c8..b1d0aa64 100644 --- a/src/app/login/templates/login.tpl.html +++ b/src/app/login/templates/login.tpl.html @@ -1,3 +1,4 @@ +<<<<<<< HEAD
@@ -5,6 +6,15 @@

Login

+======= +
+
+ +

Login

+
+ + +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2
@@ -12,7 +22,11 @@

Login

diff --git a/src/app/login/tests/login.spec.js b/src/app/login/tests/login.spec.js index 7b4c2367..0d47d72c 100644 --- a/src/app/login/tests/login.spec.js +++ b/src/app/login/tests/login.spec.js @@ -2,7 +2,10 @@ describe('Component: Login', function() { var scope, q, loginFactory, +<<<<<<< HEAD Token_Refresh, +======= +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 oc, credentials = { Username: 'notarealusername', @@ -10,17 +13,28 @@ describe('Component: Login', function() { }; beforeEach(module('orderCloud')); beforeEach(module('orderCloud.sdk')); +<<<<<<< HEAD beforeEach(inject(function($q, $rootScope, OrderCloud, LoginService, TokenRefresh) { +======= + beforeEach(inject(function($q, $rootScope, OrderCloud, LoginService) { +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 q = $q; scope = $rootScope.$new(); loginFactory = LoginService; oc = OrderCloud; +<<<<<<< HEAD Token_Refresh = TokenRefresh; +======= +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 })); describe('Factory: LoginService', function() { var client_id; +<<<<<<< HEAD beforeEach(inject(function(clientid, TokenRefresh) { +======= + beforeEach(inject(function(clientid) { +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 client_id = clientid; })); describe('SendVerificationCode', function() { @@ -42,6 +56,39 @@ describe('Component: Login', function() { }); }); +<<<<<<< HEAD +======= + describe('Logout', function() { + var cookies, + state; + beforeEach(inject(function($cookies, $state) { + var cookieResponse = { + 'a':'1', + 'b':'2' + }; + cookies = $cookies; + state = $state; + var dfd = q.defer(); + dfd.resolve(); + spyOn(cookies, 'getAll').and.returnValue(cookieResponse); + spyOn(cookies, 'remove').and.callThrough(); + spyOn(state, 'go').and.returnValue(dfd.promise); + })); + + it('should remove all of the cookies', function() { + loginFactory.Logout(); + expect(cookies.getAll).toHaveBeenCalled(); + expect(cookies.remove).toHaveBeenCalledWith('a'); + expect(cookies.remove).toHaveBeenCalledWith('b'); + }); + + it('should redirect to home/login state', function() { + loginFactory.Logout(); + expect(state.go).toHaveBeenCalled(); + }) + }); + +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 describe('ResetPassword', function() { var creds = { ResetUsername: credentials.Username, @@ -60,6 +107,7 @@ describe('Component: Login', function() { }); describe('RememberMe', function(){ +<<<<<<< HEAD beforeEach(inject(function(){ var deferred = q.defer(); deferred.resolve(true); @@ -70,6 +118,36 @@ describe('Component: Login', function() { it('should call the TokenRefresh.GetToken method', function(){ expect(Token_Refresh.GetToken).toHaveBeenCalled(); +======= + beforeEach(function(){ + var deferred = q.defer(); + deferred.resolve({access_token:'ACCESS_TOKEN'}); + spyOn(oc.Refresh, 'GetToken').and.returnValue(deferred.promise); + + var dfd = q.defer(); + dfd.resolve(); + spyOn(oc.BuyerID, 'Set').and.returnValue(dfd.promise); + spyOn(oc.Auth, 'SetToken').and.returnValue(dfd.promise); + + }); + + it('should find the refresh token, refresh the access token, and store the new access token in cookies', function(){ + spyOn(oc.Refresh, 'ReadToken').and.returnValue('REFRESH_TOKEN'); + loginFactory.RememberMe(); + + expect(oc.Refresh.ReadToken).toHaveBeenCalled(); + expect(oc.Refresh.GetToken).toHaveBeenCalledWith('REFRESH_TOKEN'); + scope.$digest(); + expect(oc.BuyerID.Set).toHaveBeenCalled(); + expect(oc.Auth.SetToken).toHaveBeenCalledWith('ACCESS_TOKEN'); + }); + + it('should not attempt to refresh users who do not have a refresh token', function() { + spyOn(oc.Refresh, 'ReadToken').and.returnValue(null); + loginFactory.RememberMe(); + expect(oc.Refresh.ReadToken).toHaveBeenCalled(); + expect(oc.Refresh.GetToken).not.toHaveBeenCalled(); +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 }) }); diff --git a/src/app/myAddresses/myAddresses.js b/src/app/myAddresses/myAddresses.js new file mode 100644 index 00000000..4a1b6dbb --- /dev/null +++ b/src/app/myAddresses/myAddresses.js @@ -0,0 +1,59 @@ +angular.module('orderCloud') + .config(MyAddressesConfig) + .controller('MyAddressesCtrl', MyAddressesController) +; + +function MyAddressesConfig($stateProvider) { + $stateProvider + .state('myAddresses', { + parent: 'account', + url: '/addresses', + templateUrl: 'myAddresses/templates/myAddresses.tpl.html', + controller: 'MyAddressesCtrl', + controllerAs: 'myAddresses', + data: { + pageTitle: "Personal Addresses" + }, + resolve: { + AddressList: function(OrderCloud) { + return OrderCloud.Me.ListAddresses(null, null, null, null, null, {Editable:true}); + } + } + }); +} + +function MyAddressesController(toastr, OrderCloud, ocConfirm, MyAddressesModal, AddressList) { + var vm = this; + vm.list = AddressList; + vm.create = function() { + MyAddressesModal.Create() + .then(function(data) { + toastr.success('Address Created', 'Success'); + vm.list.Items.push(data); + }); + }; + + vm.edit = function(scope){ + MyAddressesModal.Edit(scope.address) + .then(function(data) { + toastr.success('Address Saved', 'Success'); + vm.list.Items[scope.$index] = data; + }); + }; + + vm.delete = function(scope) { + vm.loading = []; + ocConfirm.Confirm("Are you sure you want to delete this address?") + .then(function() { + vm.loading[scope.$index] = OrderCloud.Me.DeleteAddress(scope.address.ID) + .then(function() { + toastr.success('Address Deleted', 'Success'); + vm.list.Items.splice(scope.$index, 1); + }) + }) + .catch(function() { + + }); + }; + +} \ No newline at end of file diff --git a/src/app/myAddresses/myAddresses.modalFactory.js b/src/app/myAddresses/myAddresses.modalFactory.js new file mode 100644 index 00000000..d782f032 --- /dev/null +++ b/src/app/myAddresses/myAddresses.modalFactory.js @@ -0,0 +1,95 @@ +angular.module('orderCloud') + .factory('MyAddressesModal', MyAddressesModalFactory) + .controller('CreateAddressModalCtrl', CreateAddressModalController) + .controller('EditAddressModalCtrl', EditAddressModalController) +; + +function MyAddressesModalFactory($uibModal) { + return { + Create: _create, + Edit: _edit + }; + + function _create() { + return $uibModal.open({ + templateUrl: 'myAddresses/templates/myAddresses.create.modal.tpl.html', + controller: 'CreateAddressModalCtrl', + controllerAs: 'createAddress', + size: 'md' + }).result; + } + + function _edit(address) { + var addressCopy = angular.copy(address); + return $uibModal.open({ + templateUrl: 'myAddresses/templates/myAddresses.edit.modal.tpl.html', + controller: 'EditAddressModalCtrl', + controllerAs: 'editAddress', + size: 'md', + resolve: { + SelectedAddress: function() { + return addressCopy; + } + } + }).result; + } +} + +function CreateAddressModalController($q, $exceptionHandler, $uibModalInstance, OrderCloud, ocGeography) { + var vm = this; + vm.countries = ocGeography.Countries; + vm.states = ocGeography.States; + vm.address = { + //defaults selected country to US + Country: 'US', + //default shipping/billing to true for personal addresses + Shipping:true, + Billing: true + }; + + vm.cancel = function() { + $uibModalInstance.dismiss(); + }; + + vm.submit = function() { + vm.loading = { + message:'Creating Address' + }; + vm.loading.promise = OrderCloud.Me.CreateAddress(vm.address) + .then(function(address) { + $uibModalInstance.close(address); + }) + .catch(function(error) { + $exceptionHandler(error); + }); + }; +} + +function EditAddressModalController($exceptionHandler, $uibModalInstance, OrderCloud, ocGeography, SelectedAddress) { + var vm = this; + vm.countries = ocGeography.Countries; + vm.states = ocGeography.States; + vm.address = SelectedAddress; + vm.addressID = angular.copy(SelectedAddress.ID); + + //default shipping/billing to true for personal addresses + vm.address.Shipping = true; + vm.address.Billing = true; + + vm.cancel = function() { + $uibModalInstance.dismiss(); + }; + + vm.submit = function() { + vm.loading = { + message:'Saving Address' + }; + vm.loading.promise = OrderCloud.Me.UpdateAddress(vm.addressID, vm.address) + .then(function(address) { + $uibModalInstance.close(address); + }) + .catch(function(error) { + $exceptionHandler(error); + }); + }; +} \ No newline at end of file diff --git a/src/app/myAddresses/templates/myAddresses.create.modal.tpl.html b/src/app/myAddresses/templates/myAddresses.create.modal.tpl.html new file mode 100644 index 00000000..e576cbc1 --- /dev/null +++ b/src/app/myAddresses/templates/myAddresses.create.modal.tpl.html @@ -0,0 +1,67 @@ + + + + + \ No newline at end of file diff --git a/src/app/myAddresses/templates/myAddresses.edit.modal.tpl.html b/src/app/myAddresses/templates/myAddresses.edit.modal.tpl.html new file mode 100644 index 00000000..d9795b6b --- /dev/null +++ b/src/app/myAddresses/templates/myAddresses.edit.modal.tpl.html @@ -0,0 +1,69 @@ +
+ + + +
\ No newline at end of file diff --git a/src/app/myAddresses/templates/myAddresses.tpl.html b/src/app/myAddresses/templates/myAddresses.tpl.html new file mode 100644 index 00000000..ec17180d --- /dev/null +++ b/src/app/myAddresses/templates/myAddresses.tpl.html @@ -0,0 +1,18 @@ +
+ +

Personal Addresses

+
+
+
+
+
+
+ Delete | + Edit +
+

+
+
+
+
+
\ No newline at end of file diff --git a/src/app/myAddresses/tests/myAddresses.modalFactory.spec.js b/src/app/myAddresses/tests/myAddresses.modalFactory.spec.js new file mode 100644 index 00000000..094b3107 --- /dev/null +++ b/src/app/myAddresses/tests/myAddresses.modalFactory.spec.js @@ -0,0 +1,120 @@ +describe('Component: myAddresses', function() { + var scope, + q, + oc, + uibModalInstance + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($rootScope, $q, OrderCloud) { + scope = $rootScope.$new(); + q = $q; + oc = OrderCloud; + })); + describe('Factory: MyAddressesModal', function() { + var uibModal, + addressModal, + createModalOptions, + editModalOptions, + actualOptions; + beforeEach(inject(function($uibModal, MyAddressesModal) { + uibModal = $uibModal; + addressModal = MyAddressesModal; + uibModalInstance = jasmine.createSpyObj('modalInstance', ['close', 'dismiss', 'result.then']); + createModalOptions = { + templateUrl: 'myAddresses/templates/myAddresses.create.modal.tpl.html', + controller: 'CreateAddressModalCtrl', + controllerAs: 'createAddress', + size: 'md' + }; + editModalOptions = { + templateUrl: 'myAddresses/templates/myAddresses.edit.modal.tpl.html', + controller: 'EditAddressModalCtrl', + controllerAs: 'editAddress', + size: 'md', + resolve: { + //we dont care what gets returned here because functions can't be + //compared anyway. We do however mock a function that captures the options + //passed in and verify they are the same, in the test. + SelectedAddress: jasmine.any(Function) + } + }; + })); + describe('Create', function() { + it('should call $uibModal open with create modal template/controller', function() { + spyOn(uibModal, 'open').and.returnValue(uibModalInstance); + addressModal.Create(); + expect(uibModal.open).toHaveBeenCalledWith(createModalOptions); + }); + }); + describe('Edit', function() { + it('should call $uibModal with edit modal template/controller', function() { + spyOn(uibModal, 'open').and.callFake(function(options) { + actualOptions = options; + return uibModalInstance; + }); + addressModal.Edit('addressToEdit'); + expect(uibModal.open).toHaveBeenCalledWith(editModalOptions); + expect(actualOptions.resolve.SelectedAddress()).toEqual('addressToEdit'); + }); + }); + }); + describe('Controller: CreateAddressModalController', function(){ + var createAddressModalCtrl + ; + beforeEach(inject(function($controller, $exceptionHandler, ocGeography){ + createAddressModalCtrl = $controller('CreateAddressModalCtrl', { + $exceptionHandler: $exceptionHandler, + $uibModalInstance: uibModalInstance, + ocGeography: ocGeography + }); + var defer = q.defer(); + defer.resolve('newAddress'); + spyOn(oc.Me, 'CreateAddress').and.returnValue(defer.promise); + })); + describe('cancel', function(){ + it('should dismiss the modal', function(){ + createAddressModalCtrl.cancel(); + expect(uibModalInstance.dismiss).toHaveBeenCalled(); + }); + }); + describe('submit', function(){ + it('should call OrderCloud.Me Create Address', function(){ + createAddressModalCtrl.submit(); + expect(oc.Me.CreateAddress).toHaveBeenCalledWith({Country:'US', Shipping: true, Billing: true}); + scope.$digest(); + expect(uibModalInstance.close).toHaveBeenCalledWith('newAddress'); + }); + }); + }); + describe('Controller: EditAddressModalCtrl', function(){ + var editAddressModalCtrl, + mockAddressResolve + ; + beforeEach(inject(function($controller, $exceptionHandler, ocGeography){ + mockAddressResolve = {name:'mockAddress', ID:'1'} + editAddressModalCtrl = $controller('EditAddressModalCtrl', { + $exceptionHandler: $exceptionHandler, + $uibModalInstance: uibModalInstance, + ocGeography: ocGeography, + SelectedAddress: mockAddressResolve + }); + var defer = q.defer(); + defer.resolve('newAddress'); + spyOn(oc.Me, 'UpdateAddress').and.returnValue(defer.promise); + })); + describe('cancel', function(){ + it('should dismiss the modal', function(){ + editAddressModalCtrl.cancel(); + expect(uibModalInstance.dismiss).toHaveBeenCalled(); + }); + }); + describe('submit', function(){ + it('should call OrderCloud.Me Update Address', function(){ + editAddressModalCtrl.submit(); + expect(oc.Me.UpdateAddress).toHaveBeenCalledWith('1', {name:'mockAddress', ID:'1', Shipping: true, Billing: true}); + scope.$digest(); + expect(uibModalInstance.close).toHaveBeenCalledWith('newAddress'); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/myAddresses/tests/myAddresses.spec.js b/src/app/myAddresses/tests/myAddresses.spec.js new file mode 100644 index 00000000..172a5449 --- /dev/null +++ b/src/app/myAddresses/tests/myAddresses.spec.js @@ -0,0 +1,101 @@ +describe('Component: myAddresses', function(){ + var scope, + q, + oc, + state + ; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($rootScope, $q, OrderCloud, $state){ + scope = $rootScope.$new(); + q = $q; + oc = OrderCloud; + state = $state; + })); + + describe('State: myAddresses', function(){ + beforeEach(inject(function($state){ + state = $state.get('myAddresses'); + spyOn(oc.Me, 'ListAddresses'); + })); + it('should resolve AddressList', inject(function($injector){ + $injector.invoke(state.resolve.AddressList); + expect(oc.Me.ListAddresses).toHaveBeenCalledWith(null, null, null, null, null, {Editable:true}); + })); + }); + + describe('Controller: MyAddressesCtrl', function(){ + var myAddressesCtrl, + state, + toaster, + confirm, + addressModal, + mockAddress, + mockAddressList + ; + beforeEach(inject(function($state, toastr, ocConfirm, MyAddressesModal, $controller){ + state = $state; + toaster = toastr; + confirm = ocConfirm; + addressModal = MyAddressesModal; + mockAddress = {ID:"MOCK_ADDRESS_ID"}; + mockAddressList = {Items:[mockAddress]}; + myAddressesCtrl = $controller('MyAddressesCtrl', { + $scope: scope, + $state: state, + toastr: toaster, + AddressList: mockAddressList + }); + spyOn(state, 'reload'); + spyOn(toaster, 'success'); + })); + it ('Should initialize the view model with the address list', function() { + expect(myAddressesCtrl.list).toEqual(mockAddressList); + }); + describe('create', function(){ + beforeEach(function(){ + var defer = q.defer(); + defer.resolve("NEW_ADDRESS"); + spyOn(addressModal, 'Create').and.returnValue(defer.promise); + myAddressesCtrl.create(); + }); + it('should call the create address modal then reload the state and display success toastr', function(){ + expect(addressModal.Create).toHaveBeenCalled(); + scope.$digest(); + expect(toaster.success).toHaveBeenCalledWith('Address Created', 'Success'); + expect(myAddressesCtrl.list).toEqual({Items:[mockAddress, "NEW_ADDRESS"]}); + + }); + }); + describe('edit', function(){ + beforeEach(function(){ + var defer = q.defer(); + defer.resolve("EDITED_ADDRESS"); + spyOn(addressModal, 'Edit').and.returnValue(defer.promise); + myAddressesCtrl.edit({$index:0, address:mockAddress}); + }); + it('should call the edit address modal, then reload the state and display success toastr', function(){ + expect(addressModal.Edit).toHaveBeenCalledWith(mockAddress); + scope.$digest(); + expect(toaster.success).toHaveBeenCalledWith('Address Saved', 'Success'); + expect(myAddressesCtrl.list).toEqual({Items:["EDITED_ADDRESS"]}); + }); + }); + describe('delete', function(){ + beforeEach(function(){ + var defer = q.defer(); + defer.resolve(); + spyOn(confirm, 'Confirm').and.returnValue(defer.promise); + spyOn(oc.Me, 'DeleteAddress').and.returnValue(defer.promise); + myAddressesCtrl.delete({$index:0, address:mockAddress}); + }); + it('should delete address, after prompting user to confirm', function(){ + expect(confirm.Confirm).toHaveBeenCalledWith('Are you sure you want to delete this address?'); + scope.$digest(); + expect(oc.Me.DeleteAddress).toHaveBeenCalledWith('MOCK_ADDRESS_ID'); + expect(toaster.success).toHaveBeenCalledWith('Address Deleted', 'Success'); + expect(myAddressesCtrl.list).toEqual({Items:[]}); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/myOrders/myOrders.js b/src/app/myOrders/myOrders.js new file mode 100644 index 00000000..b3fd1653 --- /dev/null +++ b/src/app/myOrders/myOrders.js @@ -0,0 +1,204 @@ +angular.module('orderCloud') + .config(MyOrdersConfig) + .controller('MyOrdersCtrl', MyOrdersController) + .controller('MyOrderDetailCtrl', MyOrderDetailController) + +function MyOrdersConfig($stateProvider) { + $stateProvider + .state('myOrders', { + parent: 'account', + templateUrl: 'myOrders/templates/myOrders.tpl.html', + controller: 'MyOrdersCtrl', + controllerAs: 'myOrders', + data: { + pageTitle: "Order History" + }, + url: '/myorders?from&to&search&page&pageSize&searchOn&sortBy&filters?favorites', + resolve: { + Parameters: function($stateParams, ocParameters) { + return ocParameters.Get($stateParams); + }, + OrderList: function(OrderCloud, Parameters, CurrentUser) { + if (Parameters.favorites && CurrentUser.xp.FavoriteOrders) { + Parameters.filters ? angular.extend(Parameters.filters, Parameters.filters, {ID:CurrentUser.xp.FavoriteOrders.join('|')}) : Parameters.filters = {ID:CurrentUser.xp.FavoriteOrders.join('|')}; + } else if (Parameters.filters) { + delete Parameters.filters.ID; + } + var showSubmittedOnly = angular.extend({}, Parameters.filters, {Status:'!Unsubmitted'}); + return OrderCloud.Me.ListOutgoingOrders(Parameters.search, Parameters.page, Parameters.pageSize || 12, Parameters.searchOn, Parameters.sortBy, showSubmittedOnly, Parameters.from, Parameters.to); + } + } + }) + .state('myOrders.detail', { + url: '/:orderid', + templateUrl: 'myOrders/templates/myOrders.detail.tpl.html', + controller: 'MyOrderDetailCtrl', + controllerAs: 'myOrderDetail', + resolve: { + SelectedOrder: function($stateParams, OrderCloud) { + return OrderCloud.Me.GetOrder($stateParams.orderid); + }, + SelectedPayments: function($stateParams, $q, OrderCloud) { + var deferred = $q.defer(); + OrderCloud.Payments.List($stateParams.orderid) + .then(function(data) { + var queue = []; + angular.forEach(data.Items, function(payment, index) { + if (payment.Type === 'CreditCard' && payment.CreditCardID) { + queue.push((function() { + var d = $q.defer(); + OrderCloud.Me.GetCreditCard(payment.CreditCardID) + .then(function(cc) { + data.Items[index].Details = cc; + d.resolve(); + }); + return d.promise; + })()); + } else if (payment.Type === 'SpendingAccount' && payment.SpendingAccountID) { + queue.push((function() { + var d = $q.defer(); + OrderCloud.Me.GetSpendingAccount(payment.SpendingAccountID) + .then(function(cc) { + data.Items[index].Details = cc; + d.resolve(); + }); + return d.promise; + })()); + } + }); + $q.all(queue) + .then(function() { + deferred.resolve(data); + }) + }); + + return deferred.promise; + }, + LineItemList: function($q, $stateParams, OrderCloud, ocLineItems) { + var dfd = $q.defer(); + OrderCloud.LineItems.List($stateParams.orderid, null, 1, 100) + .then(function(data) { + ocLineItems.GetProductInfo(data.Items) + .then(function() { + dfd.resolve(data); + }); + }); + return dfd.promise; + }, + PromotionList: function($stateParams, OrderCloud){ + return OrderCloud.Orders.ListPromotions($stateParams.orderid); + } + } + }); +} + +function MyOrdersController($state, $ocMedia, OrderCloud, ocParameters, OrderList, Parameters) { + var vm = this; + vm.list = OrderList; + vm.parameters = Parameters; + vm.sortSelection = Parameters.sortBy ? (Parameters.sortBy.indexOf('!') == 0 ? Parameters.sortBy.split('!')[1] : Parameters.sortBy) : null; + + //Check if filters are applied + vm.filtersApplied = vm.parameters.filters || vm.parameters.from || vm.parameters.to || ($ocMedia('max-width:767px') && vm.sortSelection); //Sort by is a filter on mobile devices + vm.showFilters = vm.filtersApplied; + + //Check if search was used + vm.searchResults = Parameters.search && Parameters.search.length > 0; + + //Reload the state with new parameters + vm.filter = function(resetPage) { + if(vm.parameters.filters && vm.parameters.filters.Status === null) delete vm.parameters.filters.Status; + $state.go('.', ocParameters.Create(vm.parameters, resetPage)); + }; + + vm.toggleFavorites = function() { + if (vm.parameters.filters && vm.parameters.filters.ID) delete vm.parameters.filters.ID; + if (vm.parameters.favorites) { + vm.parameters.favorites = ''; + } else { + vm.parameters.favorites = true; + vm.parameters.page = ''; + } + vm.filter(true); + }; + + //Reload the state with new search parameter & reset the page + vm.search = function() { + vm.filter(true); + }; + + //Clear the search parameter, reload the state & reset the page + vm.clearSearch = function() { + vm.parameters.search = null; + vm.filter(true); + }; + + //Clear relevant filters, reload the state & reset the page + vm.clearFilters = function() { + vm.parameters.filters = null; + vm.parameters.favorites = null; + vm.parameters.from = null; + vm.parameters.to = null; + $ocMedia('max-width:767px') ? vm.parameters.sortBy = null : angular.noop(); //Clear out sort by on mobile devices + vm.filter(true); + }; + + //Conditionally set, reverse, remove the sortBy parameter & reload the state + vm.updateSort = function(value) { + value ? angular.noop() : value = vm.sortSelection; + switch (vm.parameters.sortBy) { + case value: + vm.parameters.sortBy = '!' + value; + break; + case '!' + value: + vm.parameters.sortBy = null; + break; + default: + vm.parameters.sortBy = value; + } + vm.filter(false); + }; + + //Used on mobile devices + vm.reverseSort = function() { + Parameters.sortBy.indexOf('!') == 0 ? vm.parameters.sortBy = Parameters.sortBy.split('!')[1] : vm.parameters.sortBy = '!' + Parameters.sortBy; + vm.filter(false); + }; + + //Reload the state with the incremented page parameter + vm.pageChanged = function(page) { + $state.go('.', {page: page}); + }; + + //Load the next page of results with all of the same parameters + vm.loadMore = function() { + return OrderCloud.Me.ListIncomingOrders(Parameters.from, Parameters.to, Parameters.search, vm.list.Meta.Page + 1, Parameters.pageSize || vm.list.Meta.PageSize, Parameters.searchOn, Parameters.sortBy, Parameters.filters) + .then(function(data) { + vm.list.Items = vm.list.Items.concat(data.Items); + vm.list.Meta = data.Meta; + }); + }; +} + +function MyOrderDetailController($state, $exceptionHandler, toastr, OrderCloud, ocConfirm, SelectedOrder, SelectedPayments, LineItemList, PromotionList) { + var vm = this; + vm.order = SelectedOrder; + vm.list = LineItemList; + vm.paymentList = SelectedPayments.Items; + vm.canCancel = SelectedOrder.Status === 'Unsubmitted' || SelectedOrder.Status === 'AwaitingApproval'; + vm.promotionList = PromotionList.Meta ? PromotionList.Items : PromotionList; + + vm.cancelOrder = function(orderid) { + ocConfirm.Confirm('Are you sure you want to cancel this order?') + .then(function() { + OrderCloud.Orders.Cancel(orderid) + .then(function() { + $state.go('myOrders', {}, {reload: true}); + toastr.success('Order Cancelled', 'Success'); + }) + .catch(function(ex) { + $exceptionHandler(ex); + }); + }); + }; +} \ No newline at end of file diff --git a/src/app/myOrders/myOrders.md b/src/app/myOrders/myOrders.md new file mode 100644 index 00000000..fb5afd12 --- /dev/null +++ b/src/app/myOrders/myOrders.md @@ -0,0 +1,7 @@ +## MyOrders Component Overview + +This component allows you to list, edit, and delete your orders. + +You can also list, edit and delete Line Items associated with the order in this component. + +MyOrders is a buyer specific admin component. \ No newline at end of file diff --git a/src/app/myOrders/templates/myOrders.detail.tpl.html b/src/app/myOrders/templates/myOrders.detail.tpl.html new file mode 100644 index 00000000..e402d203 --- /dev/null +++ b/src/app/myOrders/templates/myOrders.detail.tpl.html @@ -0,0 +1,136 @@ +
+

+
+ + + +
+ + Order ID: {{myOrderDetail.order.ID}} +

+
+
+
+
Date Submitted: {{myOrderDetail.order.DateSubmitted | date:'short'}}
+
Subtotal: {{myOrderDetail.order.Subtotal | currency}}
+
Shipping: + {{myOrderDetail.order.ShippingCost | currency}}
+
Tax: + {{myOrderDetail.order.TaxCost | currency}}
+
+ {{promotion.Code}} + - {{promotion.Amount | currency}}
+
+

Total: {{myOrderDetail.order.Total | currency}}

+
+

+ +

+
+
+

{{payment.Type | humanize}} {{payment.Amount | currency}}

+ +

PO#: {{payment.xp.PONumber}}

+ +

+ + XXXX-XXXX-XXXX-{{payment.Details.PartialAccountNumber}} +

+ +

+ {{payment.Details.Name}}
+ Remaining Balance: {{payment.Details.Balance | currency}} +

+
+
+
+
+
Delivery Address
+
+
+

+
+
+
Billing Address
+
+
+

+
+
+
+
+
+

+ +

+
+ +
+
+
+ {{lineItem.Product.xp.image.Name || 'Product Image'}} +
+
+
+
+
+

+ {{lineItem.Product.Name}} +

+ {{lineItem.ProductID}} +
    +
  • + {{spec.Name}}: + {{spec.Value}} +
  • +
+
+
+
+
+

{{lineItem.UnitPrice | currency}}

+
+
+

+ {{lineItem.Quantity}} +

+ + {{'x ' + lineItem.Product.QuantityMultiplier + (lineItem.Quantity ? (' (' + (lineItem.Quantity * lineItem.Product.QuantityMultiplier) + ')') : '')}} + +
+
+

{{lineItem.LineTotal | currency}}

+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/myOrders/templates/myOrders.tpl.html b/src/app/myOrders/templates/myOrders.tpl.html new file mode 100644 index 00000000..0b99914f --- /dev/null +++ b/src/app/myOrders/templates/myOrders.tpl.html @@ -0,0 +1,146 @@ +
+

Order History

+
+ +
+
+ +
+
+ + + + + + + + +
+
+
+
+
+ +
+ +
+ +
+
+
+
+
+
+ + +
+
+
+ +
+ + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+
+ +
+ No matches found. +
+
+ {{(application.$ocMedia('min-width:768px') ? myOrders.list.Meta.ItemRange[0] : '1') + ' - ' + myOrders.list.Meta.ItemRange[1] + ' of ' + myOrders.list.Meta.TotalCount + ' results'}} + + + + + + + + + + + + + + + + + + + + + +
+ + ID + + + + + + Status + + + + + + Date Submitted + + + + Total
+ + {{order.ID}}{{order.Status}}{{(order.DateSubmitted || order.DateCreated) | date}}{{order.Total | currency}} + +
+
+ +
+ +
+
\ No newline at end of file diff --git a/src/app/myOrders/tests/myOrders.spec.js b/src/app/myOrders/tests/myOrders.spec.js new file mode 100644 index 00000000..bb4ac9c0 --- /dev/null +++ b/src/app/myOrders/tests/myOrders.spec.js @@ -0,0 +1,236 @@ +describe('Component: MyOrders', function() { + var scope, + q, + oc, + _ocParameters, + _ocLineItems, + mockParams, + state, + currentUser; + beforeEach(module(function($provide) { + mockParams = {search:null, page: null, pageSize: null, searchOn: null, sortBy: null, filters: null, from: null, to: null, favorites: null}; + $provide.value('Parameters', mockParams); + $provide.value('CurrentUser', {ID:"FAKE_USER"}); + })); + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($q, $rootScope, OrderCloud, ocParameters, ocLineItems, CurrentUser, $state) { + q = $q; + scope = $rootScope.$new(); + oc = OrderCloud; + _ocParameters = ocParameters; + _ocLineItems = ocLineItems; + state = $state; + currentUser = CurrentUser; + })); + + describe('State: myOrders', function() { + var state; + beforeEach(inject(function($state) { + state = $state.get('myOrders'); + spyOn(_ocParameters, 'Get'); + spyOn(oc.Me, 'ListOutgoingOrders'); + })); + it('should resolve Parameters', inject(function($injector){ + $injector.invoke(state.resolve.Parameters); + expect(_ocParameters.Get).toHaveBeenCalled(); + })); + it('should resolve OrderList', inject(function($injector) { + mockParams.filters = {Status:'!Unsubmitted'}; + mockParams.pageSize = 12; + $injector.invoke(state.resolve.OrderList); + expect(oc.Me.ListOutgoingOrders).toHaveBeenCalledWith(null, null, 12, null, null, {Status:'!Unsubmitted'}, null, null); + })); + }); + describe('State: myOrders.detail', function() { + var state, + stateParams, + mockPaymentList + ; + beforeEach(inject(function($state, $stateParams) { + stateParams = $stateParams; + stateParams.orderid = 'FAKE_ORDER'; + state = $state.get('myOrders.detail'); + + mockPaymentList = { + Items: [{ + Type:'CreditCard', + CreditCardID:'CreditCardID123' + }] + }; + var defer = q.defer(); + defer.resolve(mockPaymentList); + + var lidefer = q.defer(); + defer.resolve({Items:[]}); + + spyOn(oc.Me, 'GetOrder'); + spyOn(oc.Payments, 'List').and.returnValue(defer.promise); + spyOn(oc.Me, 'GetCreditCard'); + spyOn(oc.LineItems, 'List').and.returnValue(lidefer.promise); + spyOn(oc.Orders, 'ListPromotions'); + })); + it('should resolve SelectedOrder', inject(function($injector) { + $injector.invoke(state.resolve.SelectedOrder); + expect(oc.Me.GetOrder).toHaveBeenCalledWith(stateParams.orderid); + })); + it('should resolve SelectedPayments', inject(function($injector){ + $injector.invoke(state.resolve.SelectedPayments); + expect(oc.Payments.List).toHaveBeenCalledWith(stateParams.orderid); + scope.$digest(); + expect(oc.Me.GetCreditCard).toHaveBeenCalledWith(mockPaymentList.Items[0].CreditCardID); + })); + it('should resolve LineItemList with full product info', inject(function($injector) { + $injector.invoke(state.resolve.LineItemList); + expect(oc.LineItems.List).toHaveBeenCalledWith(stateParams.orderid, null, 1, 100); + })); + it('should resolve PromotionList', inject(function($injector){ + $injector.invoke(state.resolve.PromotionList); + expect(oc.Orders.ListPromotions).toHaveBeenCalledWith(stateParams.orderid); + })); + }); + describe('Controller: MyOrdersCtrl', function(){ + var myOrdersCtrl, + ocMedia + ; + beforeEach(inject(function($controller, $ocMedia, Parameters){ + ocMedia = $ocMedia; + myOrdersCtrl = $controller('MyOrdersCtrl', { + $state: state, + $ocMedia: ocMedia, + OrderCloud: oc, + ocParameters: _ocParameters, + OrderList: [], + Parameters: Parameters + }); + spyOn(_ocParameters, 'Create'); + spyOn(state, 'go'); + })); + describe('search', function(){ + beforeEach(function(){ + mockParams.search = 'Product1'; + myOrdersCtrl.search(); + }); + it('should reload state with search parameters: vm.parameters.search', function(){ + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith(mockParams, true); + }); + }); + describe('clearSearch', function(){ + beforeEach(function(){ + mockParams.search = 'Product1'; + myOrdersCtrl.clearSearch(); + }); + it('should reload state with search parameters cleared', function(){ + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith(mockParams, true); + }); + }); + describe('clearFilters', function(){ + var clearedParams; + beforeEach(function(){ + clearedParams = angular.copy(mockParams); + mockParams.filters = {ID:'mockID123'}; + mockParams.from = '12/01/2016'; + mockParams.to = '12/12/2016'; + myOrdersCtrl.clearFilters(); + }); + it('should reload state with the following parameters cleared: filter, from, and to', function(){ + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith(clearedParams, true); + }); + }); + describe('reverseSort', function(){ + var expectedParams; + beforeEach(function(){ + expectedParams = angular.copy(mockParams); + }); + it('if param.sortBy is !something it should reload state with param.sortBy equal to something', function(){ + mockParams.sortBy = '!Something'; + expectedParams.sortBy = 'Something'; + myOrdersCtrl.reverseSort(); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith(expectedParams, false); + }), + it('if param.sortBy is something it should reload state with param.sortBy equal to !something', function(){ + mockParams.sortBy = 'Something'; + expectedParams.sortBy = '!Something'; + myOrdersCtrl.reverseSort(); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith(expectedParams, false); + }); + }); + describe('pageChanged', function(){ + it('should reload state with selected page', function(){ + var mockPage = '3'; + myOrdersCtrl.pageChanged(mockPage); + expect(state.go).toHaveBeenCalledWith('.', {page: mockPage}); + }); + }); + describe('loadMore', function(){ + beforeEach(function(){ + var defer = q.defer(); + defer.resolve(); + spyOn(oc.Me, 'ListIncomingOrders').and.returnValue(defer.promise); + myOrdersCtrl.list = { + Meta: { + Page: 1, + PageSize: 12 + } + }; + myOrdersCtrl.loadMore(); + }); + it('should load the next page of results', function(){ + expect(oc.Me.ListIncomingOrders).toHaveBeenCalledWith(null, null, null, 2, 12, null, null, null); + }); + }); + }); + describe('Controller: MyOrderDetailCtrl', function() { + var orderDetailCtrl, + confirm, + toaster, + mockOrderID + ; + beforeEach(inject(function($controller, toastr, ocConfirm) { + toaster = toastr; + confirm = ocConfirm; + mockOrderID = 'Order123'; + orderDetailCtrl = $controller('MyOrderDetailCtrl', { + $scope: scope, + $state: state, + toaster: toastr, + OrderCloud: oc, + ocConfirm: ocConfirm, + SelectedOrder: [], + SelectedPayments: [], + LineItemList: [], + PromotionList:[] + }); + spyOn(state, 'go'); + })); + + describe('cancelOrder', function() { + beforeEach(function() { + var defer = q.defer(); + defer.resolve(); + spyOn(confirm, 'Confirm').and.returnValue(defer.promise); + spyOn(oc.Orders, 'Cancel').and.returnValue(defer.promise); + spyOn(toaster, 'success'); + orderDetailCtrl.cancelOrder(mockOrderID); + }); + it('should call OrderCloud Confirm', function() { + expect(confirm.Confirm).toHaveBeenCalledWith('Are you sure you want to cancel this order?'); + }); + it('should call Orders.Cancel', function(){ + scope.$digest(); + expect(oc.Orders.Cancel).toHaveBeenCalledWith(mockOrderID); + }); + it('should call state.go and take user to myOrders state', function(){ + scope.$digest(); + expect(state.go).toHaveBeenCalledWith('myOrders', {}, {reload: true}); + expect(toaster.success).toHaveBeenCalledWith('Order Cancelled', 'Success'); + }); + }); + }); +}); + diff --git a/src/app/myOrders/tests/myOrders.test.js b/src/app/myOrders/tests/myOrders.test.js new file mode 100644 index 00000000..e69de29b diff --git a/src/app/myPayments/myPaymentCreditCard.modalFactory.js b/src/app/myPayments/myPaymentCreditCard.modalFactory.js new file mode 100644 index 00000000..de77d5d9 --- /dev/null +++ b/src/app/myPayments/myPaymentCreditCard.modalFactory.js @@ -0,0 +1,86 @@ +angular.module('orderCloud') + .factory('MyPaymentCreditCardModal', MyPaymentCreditCardModalFactory) + .controller('CreateCreditCardModalCtrl', CreateCreditCardModalController) + .controller('EditCreditCardModalCtrl', EditCreditCardModalController) +; + +function MyPaymentCreditCardModalFactory($uibModal) { + return { + Create: _create, + Edit: _edit + }; + + function _create() { + return $uibModal.open({ + templateUrl: 'myPayments/templates/myPaymentsCreditCard.create.modal.tpl.html', + controller: 'CreateCreditCardModalCtrl', + controllerAs: 'createCreditCard', + size: 'md' + }).result; + } + + function _edit(creditCard) { + var creditCardCopy = angular.copy(creditCard); + return $uibModal.open({ + templateUrl: 'myPayments/templates/myPaymentsCreditCard.edit.modal.tpl.html', + controller: 'EditCreditCardModalCtrl', + controllerAs: 'editCreditCard', + size: 'md', + resolve: { + SelectedCreditCard: function() { + return creditCardCopy; + } + } + }).result; + } +} + +function CreateCreditCardModalController($q, $exceptionHandler, $uibModalInstance, ocCreditCardUtility, ocAuthNet) { + var vm = this; + vm.creditCardInfo = ocCreditCardUtility; + vm.creditCard = {}; + + vm.cancel = function() { + $uibModalInstance.dismiss(); + }; + + vm.submit = function() { + vm.loading = { + message: 'Creating Credit Card' + }; + vm.loading.promise = ocAuthNet.CreateCreditCard(vm.creditCard) + .then(function(data){ + $uibModalInstance.close(data.ResponseBody); + }) + .catch(function(error){ + $exceptionHandler(error); + }); + }; +} + +function EditCreditCardModalController($q, $exceptionHandler, $uibModalInstance, ocCreditCardUtility, ocAuthNet, SelectedCreditCard) { + var vm = this; + vm.creditCardInfo = ocCreditCardUtility; + vm.creditCard = SelectedCreditCard; + var date = new Date(vm.creditCard.ExpirationDate); + vm.creditCard.ExpirationMonth = (date.toISOString().substring(5,7)); + vm.creditCard.ExpirationYear = date.getFullYear(); + + vm.cancel = function() { + $uibModalInstance.dismiss(); + }; + + vm.submit = function() { + //loading indicator promise + vm.loading = { + message: 'Editing Credit Card' + }; + vm.loading.promise = ocAuthNet.UpdateCreditCard(vm.creditCard) + .then(function(data){ + $uibModalInstance.close(data.ResponseBody); + }) + .catch(function(error){ + $exceptionHandler(error); + }); + }; +} diff --git a/src/app/myPayments/myPayments.js b/src/app/myPayments/myPayments.js new file mode 100644 index 00000000..4a7568ae --- /dev/null +++ b/src/app/myPayments/myPayments.js @@ -0,0 +1,67 @@ +angular.module('orderCloud') + .config(MyPaymentsConfig) + .controller('MyPaymentsCtrl', MyPaymentsController) +; + +function MyPaymentsConfig($stateProvider) { + $stateProvider + .state('myPayments', { + parent: 'account', + url: '/payments', + templateUrl: 'myPayments/templates/myPayments.tpl.html', + controller: 'MyPaymentsCtrl', + controllerAs: 'myPayments', + data: { + pageTitle: "Payment Methods" + }, + resolve: { + UserCreditCards: function(OrderCloud) { + return OrderCloud.Me.ListCreditCards(null, null, null, null, null, {'Editable':true}); + }, + UserSpendingAccounts: function(OrderCloud) { + return OrderCloud.Me.ListSpendingAccounts(null, null, null, null, null, {'RedemptionCode': '!*'}); + }, + GiftCards: function(OrderCloud) { + return OrderCloud.Me.ListSpendingAccounts(null, null, null,null, null, {'RedemptionCode': '*'}); + } + } + }); +} + +function MyPaymentsController($q, $state, toastr, $exceptionHandler, ocConfirm, ocAuthNet, MyPaymentCreditCardModal, UserCreditCards, UserSpendingAccounts, GiftCards) { + var vm = this; + vm.personalCreditCards = UserCreditCards; + vm.personalSpendingAccounts = UserSpendingAccounts; + vm.giftCards = GiftCards; + + vm.createCreditCard = function(){ + MyPaymentCreditCardModal.Create() + .then(function(data) { + toastr.success('Credit Card Created', 'Success'); + vm.personalCreditCards.Items.push(data); + }); + }; + + vm.edit = function(scope){ + MyPaymentCreditCardModal.Edit(scope.creditCard) + .then(function(data){ + toastr.success('Credit Card Updated', 'Success'); + vm.personalCreditCards.Items[scope.$index] = data; + }); + }; + + vm.delete = function(scope){ + vm.loading = []; + ocConfirm.Confirm("Are you sure you want to delete this Credit Card?") + .then(function(){ + vm.loading[scope.$index] = ocAuthNet.DeleteCreditCard(scope.creditCard) + .then(function(){ + toastr.success('Credit Card Deleted', 'Success'); + vm.personalCreditCards.Items.splice(scope.$index, 1); + }) + .catch(function(error) { + $exceptionHandler(error); + }); + }); + }; +} \ No newline at end of file diff --git a/src/app/myPayments/templates/myPayments.tpl.html b/src/app/myPayments/templates/myPayments.tpl.html new file mode 100644 index 00000000..0409a808 --- /dev/null +++ b/src/app/myPayments/templates/myPayments.tpl.html @@ -0,0 +1,92 @@ +
+
+ + +

Personal Credit Cards

+
+
+
+
+
+ +
+
+ {{creditCard.CardholderName}}
+ {{'XXXX-XXXX-XXXX-' + creditCard.PartialAccountNumber}}
+ Expires On: {{creditCard.ExpirationDate | date:'MM/yy'}} +
+
+ Delete | + Edit +
+
+
+
+
+ +
+ You have not created any personal credit cards.
+ Creat one now! +
+
+
+

Available Spending Accounts

+
+
+
+
+
+ {{spendingAccount.Name}} +
+ + Lifetime: {{spendingAccount.StartDate | date:'shortDate'}} - {{spendingAccount.EndDate | date :'shortDate'}} + + + Made Available On: {{spendingAccount.StartDate | date:'shortDate'}} + + + Expires On: {{spendingAccount.EndDate | date :'shortDate'}} + +
+
+
+ +

{{spendingAccount.Balance | currency}}

+
+
+
+
+
+
+
+

Available Gift Cards

+
+
+
+
+
+ {{giftCard.Name}}
+ Redemption Code: {{giftCard.RedemptionCode}} +
+ + Lifetime: {{giftCard.StartDate | date:'shortDate'}} - {{giftCard.EndDate | date :'shortDate'}} + + + Made Available On: {{giftCard.StartDate | date:'shortDate'}} + + + Expires On: {{giftCard.EndDate | date :'shortDate'}} + +
+
+
+

{{giftCard.Balance | currency}}

+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/myPayments/templates/myPaymentsCreditCard.create.modal.tpl.html b/src/app/myPayments/templates/myPaymentsCreditCard.create.modal.tpl.html new file mode 100644 index 00000000..c5574942 --- /dev/null +++ b/src/app/myPayments/templates/myPaymentsCreditCard.create.modal.tpl.html @@ -0,0 +1,83 @@ +
+ + + +
\ No newline at end of file diff --git a/src/app/myPayments/templates/myPaymentsCreditCard.edit.modal.tpl.html b/src/app/myPayments/templates/myPaymentsCreditCard.edit.modal.tpl.html new file mode 100644 index 00000000..7e4e73bc --- /dev/null +++ b/src/app/myPayments/templates/myPaymentsCreditCard.edit.modal.tpl.html @@ -0,0 +1,53 @@ +
+ + + +
+ + \ No newline at end of file diff --git a/src/app/myPayments/tests/myPaymentCreditCard.modalFactory.spec.js b/src/app/myPayments/tests/myPaymentCreditCard.modalFactory.spec.js new file mode 100644 index 00000000..df8c9647 --- /dev/null +++ b/src/app/myPayments/tests/myPaymentCreditCard.modalFactory.spec.js @@ -0,0 +1,152 @@ +describe('Component: myPayments', function() { + var scope, + q, + oc, + uibModalInstance + ; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($rootScope, $q, OrderCloud) { + scope = $rootScope.$new(); + q = $q; + oc = OrderCloud; + + })); + describe('Factory: MyPaymentCreditCardModal', function() { + var uibModal, + creditCardModal, + createModalOptions, + editModalOptions, + actualOptions; + beforeEach(inject(function($uibModal, MyPaymentCreditCardModal) { + uibModal = $uibModal; + creditCardModal = MyPaymentCreditCardModal; + uibModalInstance = jasmine.createSpyObj('modalInstance', ['close', 'dismiss', 'result.then']); + createModalOptions = { + templateUrl: 'myPayments/templates/myPaymentsCreditCard.create.modal.tpl.html', + controller: 'CreateCreditCardModalCtrl', + controllerAs: 'createCreditCard', + size: 'md' + }; + editModalOptions = { + templateUrl: 'myPayments/templates/myPaymentsCreditCard.edit.modal.tpl.html', + controller: 'EditCreditCardModalCtrl', + controllerAs: 'editCreditCard', + size: 'md', + resolve: { + //we dont care what gets returned here because functions can't be + //compared anyway. We do however mock a function that captures the options + //passed in and verify they are the same, in the test. + SelectedCreditCard: jasmine.any(Function) + } + }; + })); + describe('Create', function() { + it('should call $uibModal open with create modal template/controller', function() { + spyOn(uibModal, 'open').and.returnValue(uibModalInstance); + creditCardModal.Create(); + expect(uibModal.open).toHaveBeenCalledWith(createModalOptions); + }); + }); + describe('Edit', function() { + it('should call $uibModal with edit modal template/controller', function() { + spyOn(uibModal, 'open').and.callFake(function(options) { + actualOptions = options; + return uibModalInstance; + }); + creditCardModal.Edit('addressToEdit'); + expect(uibModal.open).toHaveBeenCalledWith(editModalOptions); + expect(actualOptions.resolve.SelectedCreditCard()).toEqual('addressToEdit'); + }); + }); + }); + describe('Controller: CreateCreditCardModalController', function(){ + var CreateCreditCardModalCtrl, + authNet, + data + ; + beforeEach(inject(function($controller, $exceptionHandler, ocAuthNet, ocCreditCardUtility){ + authNet = ocAuthNet; + data = { ResponseBody : {}}; + CreateCreditCardModalCtrl = $controller('CreateCreditCardModalCtrl', { + $q : q, + $exceptionHandler: $exceptionHandler, + $uibModalInstance: uibModalInstance, + ocCreditCardUtility: ocCreditCardUtility, + ocAuthNet : authNet + }); + var defer = q.defer(); + defer.resolve(data); + spyOn(authNet, 'CreateCreditCard').and.returnValue(defer.promise); + })); + describe('cancel', function(){ + it('should dismiss the modal', function(){ + CreateCreditCardModalCtrl.cancel(); + expect(uibModalInstance.dismiss).toHaveBeenCalled(); + }); + }); + describe('submit', function(){ + it('should call ocAuthNet Create Credit Card then close modal', function(){ + CreateCreditCardModalCtrl.submit(); + expect(authNet.CreateCreditCard).toHaveBeenCalledWith({}); + scope.$digest(); + expect(uibModalInstance.close).toHaveBeenCalledWith(data.ResponseBody); + }); + }); + }); + + describe('Controller: EditCreditCardModalController', function(){ + var EditCreditCardModalCtrl, + authNet, + mockCCResolve, + ccUtility + ; + + beforeEach(inject(function($controller, $exceptionHandler,ocCreditCardUtility, ocAuthNet ){ + mockCCResolve = { + CardType: "Visa", + CardholderName: "Test Card", + DateCreated: "2016-12-09T23:13:17.773+00:00", + Editable: true, + ExpirationDate: "2018-04-01T00:00:00+00:00", + ExpirationMonth: "04", + ExpirationYear: 2018, + ID: "ocV_h0lRuk27xC5CuZ5FEA", + PartialAccountNumber: "4555", + Token: "1802947170" + }; + ccUtility = ocCreditCardUtility; + authNet = ocAuthNet; + exceptionHandler = $exceptionHandler; + + EditCreditCardModalCtrl = $controller('EditCreditCardModalCtrl', { + $q: q, + $exceptionHandler: exceptionHandler, + $uibModalInstance: uibModalInstance, + ocAuthNet: authNet, + ocCreditCardUtility: ccUtility, + SelectedCreditCard: mockCCResolve + }); + var defer = q.defer(); + defer.resolve({}); + spyOn(authNet, 'UpdateCreditCard').and.returnValue(defer.promise); + })); + describe('cancel', function(){ + it('should dismiss the modal', function(){ + EditCreditCardModalCtrl.cancel(); + expect(uibModalInstance.dismiss).toHaveBeenCalled(); + }); + }); + describe('submit', function(){ + it('should call ocAuthNet Update Credit Card', function(){ + EditCreditCardModalCtrl.submit(); + expect(authNet.UpdateCreditCard).toHaveBeenCalledWith(mockCCResolve); + scope.$digest(); + expect(uibModalInstance.close).toHaveBeenCalledWith({}); + }); + }); + + }); + + +}); \ No newline at end of file diff --git a/src/app/myPayments/tests/myPayments.spec.js b/src/app/myPayments/tests/myPayments.spec.js new file mode 100644 index 00000000..7382abac --- /dev/null +++ b/src/app/myPayments/tests/myPayments.spec.js @@ -0,0 +1,163 @@ +describe('Component: myPayments', function() { + var scope, + q, + oc; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($q, $rootScope, OrderCloud) { + scope = $rootScope.$new(); + q = $q; + oc = OrderCloud; + })); + + describe('State: myPayments', function() { + var state; + beforeEach(inject(function($state) { + state = $state.get('myPayments'); + spyOn(oc.Me, 'ListCreditCards').and.returnValue(null); + spyOn(oc.Me, 'ListSpendingAccounts').and.returnValue(null); + })); + it('should resolve UserCreditCards', inject(function($injector){ + $injector.invoke(state.resolve.UserCreditCards); + expect(oc.Me.ListCreditCards).toHaveBeenCalledWith(null, null, null, null, null,{'Editable': true}); + })); + it('should resolve UserSpendingAccounts', inject(function($injector) { + $injector.invoke(state.resolve.UserSpendingAccounts); + expect(oc.Me.ListSpendingAccounts).toHaveBeenCalledWith(null, null, null, null, null, {'RedemptionCode': '!*'}); + })); + it('should resolve UserSpendingAccounts', inject(function($injector) { + $injector.invoke(state.resolve.GiftCards); + expect(oc.Me.ListSpendingAccounts).toHaveBeenCalledWith(null, null, null, null, null, {'RedemptionCode': '*'}); + })); + }); + + describe('Controller: MyPaymentsController', function() { + var myPaymentCtrl, + state, + toaster, + exceptionHandler, + confirm, + creditCardModal, + authNet, + mockCreditCard, + mockSpendingAccount, + mockGiftCard, + mockCCresponse, + mockSAresponse, + mockGCresponse + ; + + beforeEach(inject(function($controller, $q, $state, toastr, $exceptionHandler, ocConfirm, ocAuthNet, MyPaymentCreditCardModal) { + state = $state; + toaster = toastr; + exceptionHandler = $exceptionHandler; + confirm = ocConfirm; + authNet = ocAuthNet; + mockCreditCard = { + "ID": "testCompanyACard", + "Editable": true, + "Token": null, + "DateCreated": "2016-12-07T17:49:28.73+00:00", + "CardType": "visa", + "PartialAccountNumber": "123", + "CardholderName": "CompanyA", + "ExpirationDate": "2016-02-20T00:00:00+00:00", + "xp": null + }; + mockSpendingAccount = { + "ID": "1bXwQHDke0SF4LRPzCpDcQ", + "Name": "Gift Card Expires Next Month", + "Balance": 20, + "AllowAsPaymentMethod": true, + "RedemptionCode": null, + "StartDate": "2016-12-01T00:00:00+00:00", + "EndDate": "2017-02-02T00:00:00+00:00", + "xp": null + }; + mockGiftCard = { + "ID": "1bXwQHDke0SF4LRPzCpDcQ", + "Name": "Gift Card Expires Next Month", + "Balance": 20, + "AllowAsPaymentMethod": true, + "RedemptionCode": "Hello", + "StartDate": "2016-12-01T00:00:00+00:00", + "EndDate": "2017-02-02T00:00:00+00:00", + "xp": null + }; + mockCCresponse = {Items:[mockCreditCard]}; + mockSAresponse = {Items:[mockSpendingAccount]}; + mockGCresponse = {Items:[mockGiftCard]}; + creditCardModal = MyPaymentCreditCardModal; + myPaymentCtrl = $controller('MyPaymentsCtrl', { + $scope : scope, + $state : state, + toastr : toaster, + UserCreditCards : mockCCresponse, + MyPaymentCreditCardModal : creditCardModal, + ocConfirm : confirm, + $exceptionHandler: exceptionHandler, + UserSpendingAccounts : mockSAresponse, + GiftCards : mockGCresponse, + ocAuthNet: authNet + }); + //spyOn(state, 'reload'); + spyOn(toaster, 'success'); + })); + it ('should initialize the view model of the controller', function() { + expect(myPaymentCtrl.personalCreditCards).toEqual(mockCCresponse); + expect(myPaymentCtrl.personalSpendingAccounts).toEqual(mockSAresponse); + expect(myPaymentCtrl.giftCards).toEqual(mockGCresponse); + }); + describe('Create Credit Card', function(){ + beforeEach(function(){ + var df = q.defer(); + df.resolve("NEW_CREDIT_CARD"); + spyOn(creditCardModal, 'Create').and.returnValue(df.promise); + // spyOn(oc.Payments, 'List').and.returnValue(df.promise); + myPaymentCtrl.createCreditCard(); + }); + it('should call the create credit card modal and add the new credit card to the view model', function(){ + expect(creditCardModal.Create).toHaveBeenCalled(); + scope.$digest(); + expect(toaster.success).toHaveBeenCalledWith('Credit Card Created', 'Success'); + expect(myPaymentCtrl.personalCreditCards).toEqual({Items:[mockCreditCard, "NEW_CREDIT_CARD"]}); + }); + }); + describe('Edit a Credit Card', function(){ + beforeEach(function(){ + var df = q.defer(); + df.resolve("EDITED_CREDIT_CARD"); + spyOn(creditCardModal, 'Edit').and.returnValue(df.promise); + myPaymentCtrl.edit({$index: 0, creditCard:mockCreditCard}); + }); + it('should call the edit credit card modal then replace the old credit card in the array', function(){ + expect(creditCardModal.Edit).toHaveBeenCalledWith(mockCreditCard); + scope.$digest(); + expect(toaster.success).toHaveBeenCalledWith('Credit Card Updated', 'Success'); + expect(myPaymentCtrl.personalCreditCards).toEqual({Items:["EDITED_CREDIT_CARD"]}); + }) + }); + describe('Delete a Credit Card', function(){ + beforeEach(function(){ + var df = q.defer(); + df.resolve(); + spyOn(confirm, 'Confirm').and.returnValue(df.promise); + spyOn(authNet, 'DeleteCreditCard').and.returnValue(df.promise); + myPaymentCtrl.delete({$index:0, creditCard:mockCreditCard}); + }); + it('should call the delete credit card function, then call Authorize.Net service , then reload the state and display success toaster', function(){ + expect(confirm.Confirm).toHaveBeenCalledWith("Are you sure you want to delete this Credit Card?"); + scope.$digest(); + + expect(authNet.DeleteCreditCard).toHaveBeenCalled(); + scope.$digest(); + expect(toaster.success).toHaveBeenCalledWith('Credit Card Deleted', 'Success'); + expect(myPaymentCtrl.personalCreditCards).toEqual({Items: []}); + }) + }) + + + }); + +}); + diff --git a/src/app/myPayments/tests/myPayments.test.js b/src/app/myPayments/tests/myPayments.test.js new file mode 100644 index 00000000..e69de29b diff --git a/src/app/productBrowse/productBrowse.js b/src/app/productBrowse/productBrowse.js new file mode 100644 index 00000000..c9905b8f --- /dev/null +++ b/src/app/productBrowse/productBrowse.js @@ -0,0 +1,230 @@ +angular.module('orderCloud') + .config(ProductBrowseConfig) + .controller('ProductBrowseCtrl', ProductBrowseController) + .controller('ProductViewCtrl', ProductViewController) + .directive('preventClick', PreventClick) + .controller('MobileCategoryModalCtrl', MobileCategoryModalController) +; + +function ProductBrowseConfig($urlRouterProvider, $stateProvider) { + $urlRouterProvider.when('/browse', '/browse/products'); + $stateProvider + .state('productBrowse', { + abstract: true, + parent: 'base', + url: '/browse', + templateUrl: 'productBrowse/templates/productBrowse.tpl.html', + controller: 'ProductBrowseCtrl', + controllerAs: 'productBrowse', + data: { + pageTitle: 'Browse Products' + }, + resolve: { + Parameters: function ($stateParams, ocParameters) { + return ocParameters.Get($stateParams); + }, + CategoryList: function(OrderCloud) { + return OrderCloud.Me.ListCategories(null, 1, 100, null, null, null, 'all'); + }, + CategoryTree: function(CategoryList) { + var result = []; + angular.forEach(_.where(CategoryList.Items, {ParentID: null}), function(node) { + result.push(getnode(node)); + }); + function getnode(node) { + var children = _.where(CategoryList.Items, {ParentID: node.ID}); + if (children.length > 0) { + node.children = children; + angular.forEach(children, function(child) { + return getnode(child); + }); + } else { + node.children = []; + } + return node; + } + return result; + } + } + }) + .state('productBrowse.products', { + url: '/products?categoryid?favorites?search?page?pageSize?searchOn?sortBy?filters?depth', + templateUrl: 'productBrowse/templates/productView.tpl.html', + controller: 'ProductViewCtrl', + controllerAs: 'productView', + resolve: { + Parameters: function ($stateParams, ocParameters) { + return ocParameters.Get($stateParams); + }, + ProductList: function(OrderCloud, CurrentUser, Parameters) { + if (Parameters.favorites && CurrentUser.xp.FavoriteProducts) { + Parameters.filters ? angular.extend(Parameters.filters, Parameters.filters, {ID:CurrentUser.xp.FavoriteProducts.join('|')}) : Parameters.filters = {ID:CurrentUser.xp.FavoriteProducts.join('|')}; + } else if (Parameters.filters) { + delete Parameters.filters.ID; + } + return OrderCloud.Me.ListProducts(Parameters.search, Parameters.page, Parameters.pageSize, Parameters.searchOn, Parameters.sortBy, Parameters.filters, Parameters.categoryid); + } + } + }); +} + +function ProductBrowseController($state, $uibModal, CategoryList, CategoryTree, Parameters) { + var vm = this; + vm.parameters = Parameters; + vm.categoryList = CategoryList; + + //Category Tree Setup + vm.treeConfig = {}; + + vm.treeConfig.treeData = CategoryTree; + vm.treeConfig.treeOptions = { + equality: function(node1, node2) { + if (node2 && node1) { + return node1.ID === node2.ID; + } else { + return node1 === node2; + } + } + }; + + vm.treeConfig.selectNode = function(node) { + $state.go('productBrowse.products', {categoryid:node.ID, page:''}); + }; + + //Initiate breadcrumbs is triggered by product list view (child state "productBrowse.products") + vm.treeConfig.initBreadcrumbs = function(activeCategoryID, ignoreSetNode) { + if (!ignoreSetNode) { //first iteration of initBreadcrumbs(), initiate breadcrumb array, set selected node for tree + vm.treeConfig.selectedNode = {ID:activeCategoryID}; + vm.breadcrumb = []; + } + if (!activeCategoryID) { //at the catalog root, no expanded nodes + vm.treeConfig.expandedNodes = angular.copy(vm.breadcrumb); + } else { + var activeCategory = _.findWhere(vm.categoryList.Items, {ID: activeCategoryID}); + if (activeCategory) { + vm.breadcrumb.unshift(activeCategory); + if (activeCategory.ParentID) { + vm.treeConfig.initBreadcrumbs(activeCategory.ParentID, true); + } else { //last iteration, set tree expanded nodes to the breadcrumb + vm.treeConfig.expandedNodes = angular.copy(vm.breadcrumb); + } + } + } + }; + + vm.toggleFavorites = function() { + if (vm.parameters.filters && vm.parameters.filters.ID) delete vm.parameters.filters.ID; + if (vm.parameters.favorites) { + vm.parameters.favorites = ''; + } else { + vm.parameters.favorites = true; + vm.parameters.page = ''; + } + $state.go('productBrowse.products', vm.parameters); + }; + + vm.openCategoryModal = function(){ + $uibModal.open({ + animation: true, + backdrop:'static', + templateUrl: 'productBrowse/templates/mobileCategory.modal.tpl.html', + controller: 'MobileCategoryModalCtrl', + controllerAs: 'mobileCategoryModal', + size: '-full-screen', + resolve: { + TreeConfig: function () { + return vm.treeConfig; + } + } + }) + .result.then(function(node){ + $state.go('productBrowse.products', {categoryid:node.ID, page:''}); + }); + }; +} + +function ProductViewController($state, $ocMedia, ocParameters, OrderCloud, CurrentOrder, ProductList, CategoryList, Parameters){ + var vm = this; + vm.parameters = Parameters; + vm.categories = CategoryList; + vm.list = ProductList; + + vm.sortSelection = Parameters.sortBy ? (Parameters.sortBy.indexOf('!') == 0 ? Parameters.sortBy.split('!')[1] : Parameters.sortBy) : null; + + //Filtering and Search Functionality + //check if filters are applied + vm.filtersApplied = vm.parameters.filters || ($ocMedia('max-width: 767px') && vm.sortSelection); + vm.showFilters = vm.filtersApplied; + + + //reload the state with new filters + vm.filter = function(resetPage) { + $state.go('.', ocParameters.Create(vm.parameters, resetPage)); + }; + + //clear the relevant filters, reload the state & reset the page + vm.clearFilters = function() { + vm.parameters.filters = null; + $ocMedia('max-width: 767px') ? vm.parameters.sortBy = null : angular.noop(); + vm.filter(true); + }; + + vm.updateSort = function(value) { + value ? angular.noop() : value = vm.sortSelection; + switch (vm.parameters.sortBy) { + case value: + vm.parameters.sortBy = '!' + value; + break; + case '!' + value: + vm.parameters.sortBy = null; + break; + default: + vm.parameters.sortBy = value; + } + vm.filter(false); + }; + + vm.reverseSort = function() { + Parameters.sortBy.indexOf('!') == 0 ? vm.parameters.sortBy = Parameters.sortBy.split('!')[1] : vm.parameters.sortBy = '!' + Parameters.sortBy; + vm.filter(false); + }; + + //reload the state with the incremented page parameter + vm.pageChanged = function() { + $state.go('.', { + page: vm.list.Meta.Page + }); + }; + + //load the next page of results with all the same parameters + vm.loadMore = function() { + return OrderCloud.Me.ListProducts(Parameters.search, vm.list.Meta.Page + 1, Parameters.pageSize || vm.list.Meta.PageSize, Parameters.searchOn, Parameters.sortBy, Parameters.filters) + .then(function(data) { + vm.list.Items = vm.list.Items.concat(data.Items); + vm.list.Meta = data.Meta; + }); + }; +} + +function PreventClick(){ + return { + link: function($scope, element) { + element.on("click", function(e){ + e.stopPropagation(); + }); + } + }; +} + +function MobileCategoryModalController($uibModalInstance, TreeConfig){ + var vm = this; + vm.treeConfig = TreeConfig; + + vm.cancel = function() { + $uibModalInstance.dismiss(); + }; + + vm.selectNode = function(node){ + $uibModalInstance.close(node); + }; +} \ No newline at end of file diff --git a/src/app/productBrowse/templates/mobileCategory.modal.tpl.html b/src/app/productBrowse/templates/mobileCategory.modal.tpl.html new file mode 100644 index 00000000..e2fcbfb8 --- /dev/null +++ b/src/app/productBrowse/templates/mobileCategory.modal.tpl.html @@ -0,0 +1,14 @@ + + diff --git a/src/app/productBrowse/templates/productBrowse.tpl.html b/src/app/productBrowse/templates/productBrowse.tpl.html new file mode 100644 index 00000000..6bc423f9 --- /dev/null +++ b/src/app/productBrowse/templates/productBrowse.tpl.html @@ -0,0 +1,51 @@ + diff --git a/src/app/productBrowse/templates/productView.tpl.html b/src/app/productBrowse/templates/productView.tpl.html new file mode 100644 index 00000000..ce4f91d0 --- /dev/null +++ b/src/app/productBrowse/templates/productView.tpl.html @@ -0,0 +1,61 @@ + +
+
+
+ +
+ +
+ +
+
+
+
+ +
{{(application.$ocMedia('min-width:768px') ? productView.list.Meta.ItemRange[0] : '1') + ' - ' + productView.list.Meta.ItemRange[1] + ' of ' + productView.list.Meta.TotalCount + ' results'}}
+
+
+ + + +
+
+ No products found. +
+
+
+
+
+
+
+
+ + +
+ +
+ + \ No newline at end of file diff --git a/src/app/productBrowse/tests/productBrowse.spec.js b/src/app/productBrowse/tests/productBrowse.spec.js new file mode 100644 index 00000000..75704458 --- /dev/null +++ b/src/app/productBrowse/tests/productBrowse.spec.js @@ -0,0 +1,97 @@ +describe('Component: ProductBrowse', function(){ + var oc, + parameters, + currentUser, + productList, + categoryList; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(module(function($provide) { + $provide.value('Parameters', {search:null, page: null, pageSize: null, searchOn: null, sortBy: null, userID: null, userGroupID: null, level: null, buyerID: null}) + $provide.value('CurrentUser', {}); + $provide.value('ProductList', {}); + $provide.value('CategoryList', {}); + })); + beforeEach(inject(function(OrderCloud, Parameters, ProductList, CategoryList){ + oc = OrderCloud; + parameters = Parameters; + currentUser = { + ID: "U01", + Username: "User01", + FirstName: "Test", + LastName: "User 01", + Email: "test@four51.com", + Phone: "5555555555", + TermsAccepted: null, + Active: true, + xp: { + FavoriteProducts: [] + }, + AvailableRoles: [ + "FullAccess" + ] + }; + productList = ProductList; + categoryList = CategoryList; + })); + + describe('State: productBrowse', function(){ + var state; + beforeEach(inject(function($state, ocParameters){ + state = $state.get('productBrowse'); + spyOn(ocParameters, 'Get').and.returnValue(null); + spyOn(oc.Me, 'ListCategories').and.returnValue(null); + })); + it('should resolve Parameters', inject(function($injector, ocParameters){ + $injector.invoke(state.resolve.Parameters); + expect(ocParameters.Get).toHaveBeenCalled(); + })); + it('should resolve CategoryList', inject(function($injector){ + $injector.invoke(state.resolve.CategoryList); + expect(oc.Me.ListCategories).toHaveBeenCalledWith(null, 1, 100, null, null, null, 'all'); + })); + }); + describe('State: productBrowse.products', function(){ + var state; + beforeEach(inject(function($state, ocParameters){ + state = $state.get('productBrowse.products'); + spyOn(ocParameters, 'Get').and.returnValue(null); + spyOn(oc.Me, 'ListProducts').and.returnValue(null); + })); + it('should resolve Parameters', inject(function($injector, ocParameters){ + $injector.invoke(state.resolve.Parameters); + expect(ocParameters.Get).toHaveBeenCalled(); + })); + it('should resolve ProductList', inject(function($injector){ + $injector.invoke(state.resolve.ProductList); + expect(oc.Me.ListProducts).toHaveBeenCalledWith(parameters.search, parameters.page, parameters.pageSize, parameters.searchOn, parameters.sortBy, parameters.filters, parameters.categoryid); + })); + }); + //describe('Controller: ProductViewCtrl', function(){ + // var productViewCtrl; + // beforeEach(inject(function($state, $controller){ + // productViewCtrl = $controller('ProductViewCtrl', { + // ProductList: productList, + // CategoryList: categoryList + // }); + // })); + // describe('LoadMore', function(){ + // beforeEach(function(){ + // productViewCtrl.list = { + // Meta: { + // Page: '', + // PageSize: '' + // }, + // Items: {} + // }; + // productViewCtrl.productList = productList; + // productViewCtrl.categoryList = categoryList; + // spyOn(oc.Me, 'ListProducts').and.returnValue(null); + // productViewCtrl.loadMore(); + // }); + // it('should call the Me ListProducts method', function(){ + // expect(oc.Me.ListProducts).toHaveBeenCalledWith(parameters.search, productViewCtrl.list.Meta.Page + 1, parameters.pageSize || productViewCtrl.list.Meta.PageSize, parameters.searchOn, parameters.sortBy, parameters.filters); + // }); + // }); + //}); +}); \ No newline at end of file diff --git a/src/app/productDetail/README.md b/src/app/productDetail/README.md new file mode 100644 index 00000000..57d9dbdf --- /dev/null +++ b/src/app/productDetail/README.md @@ -0,0 +1 @@ +Product Detail diff --git a/src/app/productDetail/productDetail.js b/src/app/productDetail/productDetail.js new file mode 100644 index 00000000..16b0cf99 --- /dev/null +++ b/src/app/productDetail/productDetail.js @@ -0,0 +1,47 @@ +angular.module('orderCloud') + .config(ProductConfig) + .controller('ProductDetailCtrl', ProductDetailController) +; + +function ProductConfig($stateProvider) { + $stateProvider + .state('productDetail', { + parent: 'base', + url: '/product/:productid', + templateUrl: 'productDetail/templates/productDetail.tpl.html', + controller: 'ProductDetailCtrl', + controllerAs: 'productDetail', + resolve: { + Product: function ($stateParams, OrderCloud) { + return OrderCloud.Me.GetProduct($stateParams.productid); + } + } + }); +} + + +function ProductDetailController($exceptionHandler, Product, CurrentOrder, ocLineItems, toastr) { + var vm = this; + vm.item = Product; + vm.finalPriceBreak = null; + + vm.addToCart = function() { + ocLineItems.AddItem(CurrentOrder, vm.item) + .then(function(){ + toastr.success('Product added to cart', 'Success') + }) + .catch(function(error){ + $exceptionHandler(error); + }); + }; + + vm.findPrice = function(qty){ + angular.forEach(vm.item.StandardPriceSchedule.PriceBreaks, function(priceBreak) { + if (priceBreak.Quantity <= qty) + vm.finalPriceBreak = angular.copy(priceBreak); + }); + + return vm.finalPriceBreak.Price * qty; + }; +} + diff --git a/src/app/productDetail/specForm/specForms.js b/src/app/productDetail/specForm/specForms.js new file mode 100644 index 00000000..aca82879 --- /dev/null +++ b/src/app/productDetail/specForm/specForms.js @@ -0,0 +1,48 @@ +angular.module('orderCloud') + .directive('ocSpecForm', OCSpecForm) + .directive('specSelectField', SpecSelectionDirective) +; + +function OCSpecForm(OrderCloud) { + return { + scope: { + product: '=' + }, + templateUrl: 'productDetail/specForm/templates/specForm.tpl.html', + replace: true, + link: function(scope){ + if (scope.product.SpecCount > 0) { + OrderCloud.Me.ListSpecs(scope.product.ID, null, 1, 100) + .then(function(data){ + //go through specs array if there is a default value, set the specValue = default value + angular.forEach(data.Items, function(obj, key){ + obj.DefaultValue && (obj.AllowOpenText == false) ? obj.Value = obj.DefaultValue : angular.noop(); + }); + scope.product.Specs=data.Items; + }); + } else { + return null; + } + } + } +} +// this directive makes it so that when an option selected , it checks that option selected to see if it has an openFieldText so that an input will appear so user can enter value. +function SpecSelectionDirective() { + return { + scope: { + spec: '=' + }, + templateUrl: 'productDetail/specForm/templates/specSelectionField.tpl.html', + link: function(scope) { + if(scope.spec.DefaultOptionID) scope.spec.OptionID = scope.spec.DefaultOptionID; + scope.showField = false; + scope.$watch(function() { + return scope.spec.OptionID; + }, function(newVal, oldVal) { + if (!newVal) return; + var selectedOption = _.findWhere(scope.spec.Options, {ID : newVal}); + scope.showField= selectedOption.IsOpenText; + }); + } + }; +} \ No newline at end of file diff --git a/src/app/productDetail/specForm/templates/specForm.tpl.html b/src/app/productDetail/specForm/templates/specForm.tpl.html new file mode 100644 index 00000000..e2bcc1b8 --- /dev/null +++ b/src/app/productDetail/specForm/templates/specForm.tpl.html @@ -0,0 +1,15 @@ +
+
+ +

{{spec.DefaultValue}}

+ +
+
+ + +
+
diff --git a/src/app/productDetail/specForm/templates/specSelectionField.tpl.html b/src/app/productDetail/specForm/templates/specSelectionField.tpl.html new file mode 100644 index 00000000..12d6e131 --- /dev/null +++ b/src/app/productDetail/specForm/templates/specSelectionField.tpl.html @@ -0,0 +1,19 @@ +
+
+ +
+
+ +
+
diff --git a/src/app/productDetail/templates/productDetail.tpl.html b/src/app/productDetail/templates/productDetail.tpl.html new file mode 100644 index 00000000..e4ea813b --- /dev/null +++ b/src/app/productDetail/templates/productDetail.tpl.html @@ -0,0 +1,51 @@ +
+

{{productDetail.item.Name || productDetail.item.ID}} {{productDetail.item.ID}} + +

+
+
+
+ +
+ {{productDetail.item.xp.image.Name || 'Product Image'}} +
+
+
+
+
+

Description

+
+
+ {{productDetail.item.Description}} +
+
+
+
+

Price Breaks

+
+ + + + + + + + + + + + + +
QuantityPrice
{{priceBreak.Quantity}}{{priceBreak.Price | currency}}
+
+ +
+ +
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/app/productDetail/test/productView.spec.js b/src/app/productDetail/test/productView.spec.js new file mode 100644 index 00000000..6b647679 --- /dev/null +++ b/src/app/productDetail/test/productView.spec.js @@ -0,0 +1,104 @@ +describe('Component: ProductDetail', function(){ + var scope, + oc, + mockProduct, + productResolve, + lineItemHelpers, + currentOrder + ; +//if the service your are calling in is a higher order resolve you have to mock instead of inject +// / when you are calling a service that returns a function. Mock the promise! + // when defining a controller, Key: Actual Service Value Mocked variable + + beforeEach(module('orderCloud')); + beforeEach(module(function($provide) { + $provide.value('CurrentOrder', {ID: "MockOrderID3456"}) + })); + beforeEach(inject(function($rootScope, OrderCloud, ocLineItems, CurrentOrder){ + scope = $rootScope.$new(); + oc = OrderCloud; + mockProduct = { + "ID": "MockProductID123", + "Name": "MockProductName", + "Description": "mockDescription", + "StandardPriceSchedule": { + "PriceBreaks" : [ + { + "Quantity": 2, + "Price" : 10 + } + ] + + } + }; + lineItemHelpers = ocLineItems; + currentOrder = CurrentOrder; + + })); + + describe('Configuration: ProductViewConfig', function(){ + var state, + stateParams; + + describe('State: Product',function(){ + beforeEach(inject(function($stateParams, $state){ + state = $state.get('productDetail'); + stateParams = $stateParams; + stateParams.productid = "MockProductID123"; + spyOn(oc.Me,'GetProduct'); + })); + + it('should resolve Product', inject(function($injector){ + $injector.invoke(state.resolve.Product); + expect(oc.Me.GetProduct).toHaveBeenCalledWith("MockProductID123"); + })); + }); + }); + + describe('Controller: ProductDetail', function(){ + var productDetailCtrl; + var toaster; + var q; + beforeEach(inject(function($controller, toastr, $q){ + toaster= toastr; + q = $q; + + productDetailCtrl = $controller('ProductDetailCtrl',{ + Product : mockProduct, + CurrentOrder: currentOrder, + ocLineItems: lineItemHelpers, + toastr : toaster + }); + + })); + + describe('addToCart', function(){ + beforeEach( function(){ + var defer = q.defer(); + defer.resolve(); + spyOn(lineItemHelpers,'AddItem').and.returnValue(defer.promise); + spyOn(toaster, 'success'); + productDetailCtrl.addToCart(); + }); + it('should call the ocLineItems AddItem method and display toastr', function(){ + expect(lineItemHelpers.AddItem).toHaveBeenCalledWith(currentOrder, mockProduct); + }); + it('should call toastr when successful', function(){ + scope.$digest(); + expect(toaster.success).toHaveBeenCalled(); + }); + }); + + describe('findPrice function', function(){ + //set up like this for potential addition of different quantities. + it("finalPriceBreak should equal price of Pricebreak ", function(){ + var possibleQuantities= [2]; + for(var i = 0; i ', + controller: function(ProductQuickView){ + this.quickView = function(currentorder, product){ + ProductQuickView.Open(currentorder, product); + }; + } + }) +; + +function ProductQuickViewService($uibModal) { + var service = { + Open: _open + }; + + function _open(currentOrder, product) { + return $uibModal.open({ + backdrop:'static', + templateUrl: 'productQuickView/templates/productQuickView.modal.tpl.html', + controller: 'ProductQuickViewCtrl', + controllerAs: 'productQuickView', + size: 'lg', + animation:false, + resolve: { + SelectedProduct: function() { + return product; + }, + CurrentOrder: function() { + return currentOrder; + } + } + }).result + } + + return service; +} + +function ProductQuickViewController(toastr, $uibModalInstance, SelectedProduct, CurrentOrder, ocLineItems) { + var vm = this; + vm.item = SelectedProduct; + vm.addToCart = function() { + ocLineItems.AddItem(CurrentOrder, vm.item) + .then(function(){ + toastr.success('Product added to cart', 'Success'); + $uibModalInstance.close(); + }); + }; + + vm.findPrice = function(qty){ + var finalPriceBreak = null; + angular.forEach(vm.item.StandardPriceSchedule.PriceBreaks, function(priceBreak) { + if (priceBreak.Quantity <= qty) + finalPriceBreak = angular.copy(priceBreak); + }); + + return finalPriceBreak.Price * qty; + }; + + vm.cancel = function() { + $uibModalInstance.dismiss(); + }; +} \ No newline at end of file diff --git a/src/app/productQuickView/templates/productQuickView.modal.tpl.html b/src/app/productQuickView/templates/productQuickView.modal.tpl.html new file mode 100644 index 00000000..3370735a --- /dev/null +++ b/src/app/productQuickView/templates/productQuickView.modal.tpl.html @@ -0,0 +1,53 @@ +
+ + + +
\ No newline at end of file diff --git a/src/app/productSearch/productSearch.js b/src/app/productSearch/productSearch.js new file mode 100644 index 00000000..de8e91d9 --- /dev/null +++ b/src/app/productSearch/productSearch.js @@ -0,0 +1,149 @@ +angular.module('orderCloud') + .config(ProductSearchConfig) + .controller('ProductSearchCtrl', ProductSearchController) + .controller('ProductSearchDirectiveCtrl', ProductSearchDirectiveController) + .controller('ProductSearchModalCtrl', ProductSearchModalController) + .component('ordercloudProductSearch', OrderCloudProductSearchComponent()) + .factory('ProductSearch', ProductSearchService) +; + +function ProductSearchConfig($stateProvider) { + $stateProvider + .state('productSearchResults', { + parent: 'base', + url: '/productSearchResults/:searchTerm?page&pageSize&sortBy', + templateUrl: 'productSearch/templates/productSearch.results.tpl.html', + controller: 'ProductSearchCtrl', + controllerAs: 'productSearch', + resolve: { + Parameters: function(ocParameters, $stateParams) { + return ocParameters.Get($stateParams); + }, + ProductList: function(OrderCloud, Parameters) { + return OrderCloud.Me.ListProducts(Parameters.searchTerm, Parameters.page, Parameters.pageSize || 12, null, Parameters.sortBy); + } + } + }); +} + +function ProductSearchController($state, ocParameters, Parameters, ProductList) { + var vm = this; + vm.list = ProductList; + vm.parameters = Parameters; + vm.sortSelection = Parameters.sortBy ? (Parameters.sortBy.indexOf('!') === 0 ? Parameters.sortBy.split('!')[1] : Parameters.sortBy) : null; + + //Reload the state with new parameters + vm.filter = function(resetPage) { + $state.go('.', ocParameters.Create(vm.parameters, resetPage)); + }; + + vm.updateSort = function(value) { + value ? angular.noop() : value = vm.sortSelection; + switch (vm.parameters.sortBy) { + case value: + vm.parameters.sortBy = '!' + value; + break; + case '!' + value: + vm.parameters.sortBy = null; + break; + default: + vm.parameters.sortBy = value; + } + vm.filter(false); + }; + + vm.updatePageSize = function(pageSize) { + vm.parameters.pageSize = pageSize; + vm.filter(true); + }; + + vm.pageChanged = function(page) { + vm.parameters.page = page; + vm.filter(false); + }; + + vm.reverseSort = function() { + Parameters.sortBy.indexOf('!') === 0 ? vm.parameters.sortBy = Parameters.sortBy.split('!')[1] : vm.parameters.sortBy = '!' + Parameters.sortBy; + vm.filter(false); + }; +} + +function OrderCloudProductSearchComponent() { + return { + replace:true, + templateUrl: 'productSearch/templates/productSearch.component.tpl.html', + controller: 'ProductSearchDirectiveCtrl', + bindings: { + maxProducts: '<' + } + }; +} + +function ProductSearchDirectiveController($state, OrderCloud) { + var vm = this; + + vm.getSearchResults = function() { + return OrderCloud.Me.ListProducts(vm.searchTerm, 1, vm.maxProducts || 5) + .then(function(data) { + return data.Items; + }); + }; + + vm.onSelect = function(productID) { + $state.go('productDetail', { + productid: productID + }); + }; + + vm.onHardEnter = function(searchTerm) { + $state.go('productSearchResults', { + searchTerm: searchTerm + }); + }; +} + +function ProductSearchService($uibModal) { + var service = { + Open:_open + }; + + function _open() { + return $uibModal.open({ + backdrop:'static', + templateUrl:'productSearch/templates/productSearch.modal.tpl.html', + controller: 'ProductSearchModalCtrl', + controllerAs: '$ctrl', + size: '-full-screen c-productsearch-modal' + }).result + } + + return service; +} + +function ProductSearchModalController($uibModalInstance, $timeout, $scope, OrderCloud) { + var vm = this; + + $timeout(function() { + $('#ProductSearchInput').focus(); + }, 300); + + vm.getSearchResults = function() { + return OrderCloud.Me.ListProducts(vm.searchTerm, 1, vm.maxProducts || 5) + .then(function(data) { + return data.Items; + }); + }; + + //Mobile functionality + vm.cancel = function() { + $uibModalInstance.dismiss(); + }; + + vm.onSelect = function(productID) { + $uibModalInstance.close({productID: productID}); + }; + + vm.onHardEnter = function(searchTerm) { + $uibModalInstance.close({searchTerm: searchTerm}); + }; +} \ No newline at end of file diff --git a/src/app/productSearch/templates/productSearch.component.tpl.html b/src/app/productSearch/templates/productSearch.component.tpl.html new file mode 100644 index 00000000..db60d846 --- /dev/null +++ b/src/app/productSearch/templates/productSearch.component.tpl.html @@ -0,0 +1,35 @@ +
+
+ + +
+
+ No matches found +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/productSearch/templates/productSearch.modal.tpl.html b/src/app/productSearch/templates/productSearch.modal.tpl.html new file mode 100644 index 00000000..9318f1ee --- /dev/null +++ b/src/app/productSearch/templates/productSearch.modal.tpl.html @@ -0,0 +1,33 @@ + + \ No newline at end of file diff --git a/src/app/productSearch/templates/productSearch.results.tpl.html b/src/app/productSearch/templates/productSearch.results.tpl.html new file mode 100644 index 00000000..2bfc2d40 --- /dev/null +++ b/src/app/productSearch/templates/productSearch.results.tpl.html @@ -0,0 +1,60 @@ +
+ + +
+ + +
+
+
+ +
+ +
+ +
+
+
+
+ +
{{(application.$ocMedia('min-width:768px') ? productSearch.list.Meta.ItemRange[0] : '1') + ' - ' + productSearch.list.Meta.ItemRange[1] + ' of ' + productSearch.list.Meta.TotalCount + ' results'}}
+
+
+ + + +
+
+
+
+
+ +
+ No products or categories match your search. +
+ + + +
\ No newline at end of file diff --git a/src/app/productSearch/templates/productSearch.typeahead.tpl.html b/src/app/productSearch/templates/productSearch.typeahead.tpl.html new file mode 100644 index 00000000..31b08732 --- /dev/null +++ b/src/app/productSearch/templates/productSearch.typeahead.tpl.html @@ -0,0 +1,5 @@ + +

+
\ No newline at end of file diff --git a/src/app/productSearch/tests/productSearch.spec.js b/src/app/productSearch/tests/productSearch.spec.js new file mode 100644 index 00000000..15ab5ded --- /dev/null +++ b/src/app/productSearch/tests/productSearch.spec.js @@ -0,0 +1,147 @@ +describe('Component: Product Search', function(){ + var scope, + q, + oc, + state, + _ocParameters, + parameters, + mockProductList + ; + beforeEach(module(function($provide) { + $provide.value('Parameters', {searchTerm: null, page: null, pageSize: null, sortBy: null}); + })); + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($rootScope, $q, OrderCloud, ocParameters, $state, Parameters){ + scope = $rootScope.$new(); + q = $q; + oc = OrderCloud; + state = $state; + _ocParameters = ocParameters; + parameters = Parameters; + mockProductList = { + Items:['product1', 'product2'], + Meta:{ + ItemRange:[1, 3], + TotalCount: 50 + } + }; + })); + describe('State: productSearchResults', function(){ + var state; + beforeEach(inject(function($state){ + state = $state.get('productSearchResults'); + spyOn(_ocParameters, 'Get'); + spyOn(oc.Me, 'ListProducts'); + })); + it('should resolve Parameters', inject(function($injector){ + $injector.invoke(state.resolve.Parameters); + expect(_ocParameters.Get).toHaveBeenCalled(); + })); + it('should resolve ProductList', inject(function($injector){ + parameters.filters = {ParentID:'12'}; + $injector.invoke(state.resolve.ProductList); + expect(oc.Me.ListProducts).toHaveBeenCalled(); + })); + }); + describe('Controller: ProductSearchController', function(){ + var productSearchCtrl; + beforeEach(inject(function($state, $controller){ + var state = $state; + productSearchCtrl = $controller('ProductSearchCtrl', { + $state: state, + ocParameters: _ocParameters, + $scope: scope, + ProductList: mockProductList + }); + spyOn(_ocParameters, 'Create'); + spyOn(state, 'go'); + })); + describe('filter', function(){ + it('should reload state and call ocParameters.Create with any parameters', function(){ + productSearchCtrl.parameters = {pageSize: 1}; + productSearchCtrl.filter(true); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith({pageSize:1}, true); + }); + }); + describe('updateSort', function(){ + it('should reload page with value and sort order, if both are defined', function(){ + productSearchCtrl.updateSort('!ID'); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith({searchTerm: null, page: null, pageSize: null, sortBy: '!ID'}, false); + }); + it('should reload page with just value, if no order is defined', function(){ + productSearchCtrl.updateSort('ID'); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith({searchTerm: null, page: null, pageSize: null, sortBy: 'ID'}, false); + }); + }); + describe('updatePageSize', function(){ + it('should reload state with the new pageSize', function(){ + productSearchCtrl.updatePageSize('25'); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith({searchTerm: null, page: null, pageSize: '25', sortBy: null}, true); + }); + }); + describe('pageChanged', function(){ + it('should reload state with the new page', function(){ + productSearchCtrl.pageChanged('newPage'); + expect(state.go).toHaveBeenCalled(); + expect(_ocParameters.Create).toHaveBeenCalledWith({searchTerm: null, page: 'newPage', pageSize: null, sortBy: null}, false); + }); + }); + describe('reverseSort', function(){ + it('should reload state with a reverse sort call', function(){ + productSearchCtrl.parameters.sortBy = 'ID'; + productSearchCtrl.reverseSort(); + expect(_ocParameters.Create).toHaveBeenCalledWith({searchTerm: null, page: null, pageSize: null, sortBy: '!ID'}, false); + }); + }); + }); + describe('Component Directive: ordercloudProductSearch', function(){ + var productSearchComponentCtrl, + timeout + ; + beforeEach(inject(function($componentController, $timeout){ + timeout = $timeout; + productSearchComponentCtrl = $componentController('ordercloudProductSearch', { + $state:state, + $timeout: timeout, + $scope: scope, + OrderCloud:oc + }); + spyOn(state, 'go'); + })); + describe('getSearchResults', function(){ + beforeEach(function(){ + var defer = q.defer(); + defer.resolve(); + spyOn(oc.Me, 'ListProducts').and.returnValue(defer.promise); + }); + it('should call Me.ListProducts with given search term and max products', function(){ + productSearchComponentCtrl.searchTerm = 'Product1'; + productSearchComponentCtrl.maxProducts = 12; + productSearchComponentCtrl.getSearchResults(); + expect(oc.Me.ListProducts).toHaveBeenCalledWith('Product1', 1, 12); + }); + it('should default max products to five, if none is provided', function(){ + productSearchComponentCtrl.searchTerm = 'Product1'; + productSearchComponentCtrl.getSearchResults(); + expect(oc.Me.ListProducts).toHaveBeenCalledWith('Product1', 1, 5); + }); + }); + describe('onSelect', function(){ + it('should route user to productDetail state for the selected product id', function(){ + productSearchComponentCtrl.onSelect(12); + expect(state.go).toHaveBeenCalledWith('productDetail', {productid:12}); + }); + }); + describe('onHardEnter', function(){ + it('should route user to search results page for the provided search term', function(){ + productSearchComponentCtrl.onHardEnter('bikes'); + expect(state.go).toHaveBeenCalledWith('productSearchResults', {searchTerm: 'bikes'}); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/app/repeatOrder/repeatOrder.js b/src/app/repeatOrder/repeatOrder.js new file mode 100644 index 00000000..87d5b1ba --- /dev/null +++ b/src/app/repeatOrder/repeatOrder.js @@ -0,0 +1,137 @@ +angular.module('orderCloud') + .factory('RepeatOrderFactory', RepeatOrderFactory) + .controller('RepeatOrderCtrl', RepeatOrderCtrl) + .controller('RepeatOrderModalCtrl', RepeatOrderModalCtrl) + .component('ordercloudRepeatOrder', { + templateUrl: 'repeatOrder/templates/repeatOrder.component.html', + controller: RepeatOrderCtrl, + bindings: { + currentOrderId: '<', + originalOrderId: '<' + } + }); + +function RepeatOrderCtrl(toastr, RepeatOrderFactory, $uibModal) { + var vm = this; + + vm.$onInit = function() { + if (vm.orderid === 'undefined') toastr.error('repeat order component is not configured correctly. orderid is a required attribute', 'Error'); + }; + + vm.openReorderModal = function(){ + $uibModal.open({ + templateUrl: 'repeatOrder/templates/repeatOrder.modal.html', + controller: RepeatOrderModalCtrl, + controllerAs: 'repeatModal', + size: 'md', + resolve: { + OrderID: function() { + return vm.currentOrderId; + }, + LineItems: function() { + return RepeatOrderFactory.GetValidLineItems(vm.originalOrderId); + } + } + }); + }; +} + +function RepeatOrderModalCtrl(LineItems, OrderID, $uibModalInstance, $state, RepeatOrderFactory){ + var vm = this; + vm.orderid = OrderID; + vm.invalidLI = LineItems.invalid; + vm.validLI = LineItems.valid; + + + vm.cancel = function(){ + $uibModalInstance.dismiss(); + }; + + vm.submit = function(){ + vm.loading = { + templateUrl:'common/loading-indicators/templates/view.loading.tpl.html', + message:'Adding Products to Cart' + }; + vm.loading.promise = RepeatOrderFactory.AddLineItemsToCart(vm.validLI, vm.orderid) + .then(function(){ + $uibModalInstance.close(); + $state.go('cart', {}, {reload: true}); + }); + }; +} + +function RepeatOrderFactory($q, $rootScope, toastr, $exceptionHandler, OrderCloud, ocLineItems) { + return { + GetValidLineItems: getValidLineItems, + AddLineItemsToCart: addLineItemsToCart + }; + + function getValidLineItems(originalOrderID) { + var dfd = $q.defer(); + ListAllMeProducts() + .then(function(productList) { + var productIds = _.pluck(productList, 'ID'); + ocLineItems.ListAll(originalOrderID) + .then(function(lineItemList) { + lineItemList.ProductIds = productIds; + var valid = []; + var invalid = []; + angular.forEach(lineItemList, function(li) { + productIds.indexOf(li.ProductID) > -1 ? valid.push(li) : invalid.push(li); + }); + dfd.resolve({valid: valid, invalid: invalid}); + }); + }); + return dfd.promise; + + function ListAllMeProducts() { + var dfd = $q.defer(); + var queue = []; + OrderCloud.Me.ListProducts(null, 1, 100) + .then(function(data) { + var productList = data; + if (data.Meta.TotalPages > data.Meta.Page) { + var page = data.Meta.Page; + while (page < data.Meta.TotalPages) { + page += 1; + queue.push(OrderCloud.Me.ListProducts(null, page, 100)); + } + } + $q.all(queue) + .then(function(results) { + angular.forEach(results, function(result) { + productList.Items = [].concat(productList.Items, result.Items); + }); + dfd.resolve(productList.Items); + }) + .catch(function(err) { + dfd.reject(err); + }); + }); + return dfd.promise; + } + } + + function addLineItemsToCart(validLI, orderID) { + var queue = []; + var dfd = $q.defer(); + angular.forEach(validLI, function(li){ + var lineItemToAdd = { + ProductID: li.ProductID, + Quantity: li.Quantity, + Specs: li.Specs + }; + queue.push(OrderCloud.LineItems.Create(orderID, lineItemToAdd)); + }); + $q.all(queue) + .then(function(){ + dfd.resolve(); + toastr.success('Product(s) Add to Cart', 'Success'); + }) + .catch(function(error){ + $exceptionHandler(error); + dfd.reject(error); + }); + return dfd.promise; + } +} \ No newline at end of file diff --git a/src/app/repeatOrder/repeatOrder.md b/src/app/repeatOrder/repeatOrder.md new file mode 100644 index 00000000..d4273537 --- /dev/null +++ b/src/app/repeatOrder/repeatOrder.md @@ -0,0 +1,22 @@ +## RepeatOrder Component Overview + +This component can be used from the Buyer and Admin perspective and includes a repeat-order directive in the form of a button. +Clicking on this button will allow a reorder of products available to the current user. +If the products are not assigned to the current user or have been deleted they will be excluded from the reorder. + +The repeat-order directive can be placed anywhere in your HTML by including the following: + +```html + +``` + +The orderid is a required attribute for both the buyer and admin perspective. The admin perspective will additionally require +the userid and clientid. + +Below is a quick summary of attributes for this directive: +* orderid: ID of the order being reordered (required for both admin and buyer perspective) +* userid: ID of the user under which the order is being placed by (required only for admin perspective) +* clientid: the client id of the buyer app that the order is being placed by (required only for the admin perspective) +* includeshipping: will include any shipping details from the previous order if available (optional) +* includebilling: will include any billing details from the previous order if available (optional) +* claims: The claims available to the user placing the reorder. Will default to FullAccess if none are specified. (optional) diff --git a/src/app/repeatOrder/templates/repeatOrder.component.html b/src/app/repeatOrder/templates/repeatOrder.component.html new file mode 100644 index 00000000..1229e8aa --- /dev/null +++ b/src/app/repeatOrder/templates/repeatOrder.component.html @@ -0,0 +1 @@ +
Reorder
\ No newline at end of file diff --git a/src/app/repeatOrder/templates/repeatOrder.modal.html b/src/app/repeatOrder/templates/repeatOrder.modal.html new file mode 100644 index 00000000..43723fa5 --- /dev/null +++ b/src/app/repeatOrder/templates/repeatOrder.modal.html @@ -0,0 +1,36 @@ +
+ + + +
\ No newline at end of file diff --git a/src/app/repeatOrder/tests/repeatOrder.spec.js b/src/app/repeatOrder/tests/repeatOrder.spec.js new file mode 100644 index 00000000..973736ad --- /dev/null +++ b/src/app/repeatOrder/tests/repeatOrder.spec.js @@ -0,0 +1,154 @@ +describe('Component: ordercloudRepeatOrder', function(){ + var scope, + q, + state, + modalInstance, + orderID, + lineItems + ; + beforeEach(module('orderCloud')); + beforeEach(module('orderCloud.sdk')); + beforeEach(inject(function($rootScope, $q, $state) { + scope = $rootScope.$new(); + q = $q; + state = $state; + orderID = 'testOrderID123123'; + lineItems = {}; + modalInstance = jasmine.createSpyObj('modalInstance', ['close', 'dismiss', 'result.then']); + })); + describe('Controller: RepeatOrderCtrl', function(){ + var repeatOrderCtrl, + toaster; + beforeEach(inject(function($controller, toastr){ + toaster = toastr; + repeatOrderCtrl = $controller('RepeatOrderCtrl', { + toastr: toaster, + orderID: 'undefined', + lineItems: { + valid: {}, + invalid: {} + } + }) + })); + describe('$onInit', function(){ + beforeEach(function(){ + repeatOrderCtrl.orderid = 'undefined'; + spyOn(toaster, 'error'); + }); + it('should display a toastr error message', function() { + repeatOrderCtrl.$onInit(); + expect(toaster.error).toHaveBeenCalledWith('repeat order component is not configured correctly. orderid is a required attribute', 'Error'); + }) + }) + }); + describe('Controller: RepeatOrderModalCtrl', function(){ + var repeatOrderModalCtrl; + var repeatOrderFactory; + beforeEach(inject(function($controller, $state, RepeatOrderFactory){ + repeatOrderFactory = RepeatOrderFactory; + repeatOrderModalCtrl = $controller('RepeatOrderModalCtrl', { + OrderID: 'testOrderID123123', + $state: $state, + RepeatOrderFactory: repeatOrderFactory, + LineItems: { + valid: {}, + invalid: {} + }, + $uibModalInstance: modalInstance + }); + })); + describe('cancel', function() { + beforeEach(function() { + repeatOrderModalCtrl.cancel(); + }); + it('should call the modalInstance dismiss method', function(){ + expect(modalInstance.dismiss).toHaveBeenCalled(); + }) + }); + describe('submit', function(){ + beforeEach(function(){ + var defer = q.defer(); + defer.resolve(); + spyOn(repeatOrderFactory, 'AddLineItemsToCart').and.returnValue(defer.promise); + repeatOrderModalCtrl.submit(); + }); + it('should call the modalInstance close method', function(){ + scope.$digest(); + expect(modalInstance.close).toHaveBeenCalled(); + }); + it('should call the RepeatOrderFactory AddLineItemsToCart method', function(){ + expect(repeatOrderFactory.AddLineItemsToCart).toHaveBeenCalledWith(repeatOrderModalCtrl.validLI, repeatOrderModalCtrl.orderid); + }) + }) + }); + describe('Factory: RepeatOrderFactory', function(){ + var oc, + toaster, + repeatOrderFactory, + ocLIs, + originalOrderID + ; + beforeEach(inject(function(OrderCloud, toastr, RepeatOrderFactory){ + oc = OrderCloud; + toaster = toastr; + repeatOrderFactory = RepeatOrderFactory; + originalOrderID = 'testOriginalOrderID123'; + })); + describe('GetValidLineItems', function(){ + beforeEach(inject(function(ocLineItems){ + ocLIs = ocLineItems; + var meProducts = { + Meta: { + TotalPages: 1 + }, + Items: [ + { + Name: 'product1', + ID: 'productID' + } + ] + }; + var defer = q.defer(); + defer.resolve(meProducts); + spyOn(oc.Me, 'ListProducts').and.returnValue(defer.promise); + spyOn(ocLIs, 'ListAll').and.returnValue(defer.promise); + repeatOrderFactory.GetValidLineItems(originalOrderID); + })); + it('should call the OrderCloud Me ListProducts method', function(){ + expect(oc.Me.ListProducts).toHaveBeenCalledWith(null, 1, 100); + }); + it('should call lineItemHelpers ListAll method', function(){ + scope.$digest(); + expect(ocLIs.ListAll).toHaveBeenCalledWith(originalOrderID); + }) + }); + describe('AddLineItemsToCart', function(){ + beforeEach(function(){ + var validLI = [ + { + ProductID: 'productID1', + Quantity: 'productQuantity1', + Specs: 'liSpecs1' + }, + { + ProductID: 'productID2', + Quantity: 'productQuantity2', + Specs: 'liSpecs2' + } + ]; + var defer = q.defer(); + defer.resolve(); + spyOn(oc.LineItems, 'Create').and.returnValue(defer.promise); + spyOn(toaster, 'success'); + repeatOrderFactory.AddLineItemsToCart(validLI, originalOrderID); + }); + it('should call the OrderCloud LineItems Create method', function(){ + expect(oc.LineItems.Create).toHaveBeenCalledTimes(2); + }); + it('should call the toastr success method', function(){ + scope.$digest(); + expect(toaster.success).toHaveBeenCalledWith('Product(s) Add to Cart', 'Success'); + }) + }); + }); +}); \ No newline at end of file diff --git a/src/app/repeatOrder/tests/repeatOrder.test.js b/src/app/repeatOrder/tests/repeatOrder.test.js new file mode 100644 index 00000000..e69de29b diff --git a/src/app/styleguide/README.md b/src/app/styleguide/README.md new file mode 100644 index 00000000..e69de29b diff --git a/src/app/styleguide/styleguide.js b/src/app/styleguide/styleguide.js new file mode 100644 index 00000000..7d7b5f5a --- /dev/null +++ b/src/app/styleguide/styleguide.js @@ -0,0 +1,20 @@ +angular.module('orderCloud') + .config(StyleguideConfig) + .controller('StyleguideCtrl', StyleguideController) +; + +function StyleguideConfig($stateProvider) { + $stateProvider + .state('styleguide', { + parent: 'base', + url: '/styleguide', + templateUrl: 'styleguide/templates/styleguide.tpl.html', + controller: 'StyleguideCtrl', + controllerAs: 'styleguide' + }) + ; +} + +function StyleguideController() { + var vm = this; +} diff --git a/src/app/styleguide/templates/styleguide.tpl.html b/src/app/styleguide/templates/styleguide.tpl.html new file mode 100644 index 00000000..9067cb6a --- /dev/null +++ b/src/app/styleguide/templates/styleguide.tpl.html @@ -0,0 +1,698 @@ +
+
+ +
+
+

Styleguide

+

A preview of common UI elements within your application.

+
+
+

Headings +

+
+

Page Header With Small Text

+

This is an h1 heading

+

This is an h2 heading

+

This is an h3 heading

+

This is an h4 heading

+
This is an h5 heading
+
This is an h6 heading
+
+
+
+

Tables +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#First NameTables
1MichaelAre formatted like this
2LucilleDo you like them?
3Success
4Danger
5Warning
6Active
+ + + + + + + + + + + + + + + + + + + + +
#First NameTables
1MichaelThis one is bordered and condensed
2LucilleDo you still like it?
+
+
+
+

Content Formatting +

+
+

This is a lead paragraph.

+

This is an ordinary paragraph that is long enough to wrap to + multiple lines so that you can see how the line spacing looks.

+

Muted color paragraph.

+

Warning color paragraph.

+

Danger color paragraph.

+

Info color paragraph.

+

Success color paragraph.

+

This is text in a small wrapper. NBD, right?

+
+
Twitter, Inc.
795 Folsom Ave, Suite 600
San Francisco, CA 94107
P: (123) 456-7890
+
Full Name
first.last@example.com
+
+
Here's what a blockquote looks like in Bootstrap. Use small to identify the source. +
+
+
+
+
    +
  • Normal Unordered List
  • +
  • Can Also Work +
      +
    • With Nested Children
    • +
    +
  • +
  • Adds Bullets to Page
  • +
+
+
+
    +
  1. Normal Ordered List
  2. +
  3. Can Also Work +
      +
    1. With Nested Children
    2. +
    +
  4. +
  5. Adds Bullets to Page
  6. +
+
+
+
+
function preFormatting() { // looks like this; var something = somethingElse; return true;}
+
+
+
+

Forms +

+
+
+
+ Legend +
+ + +
+
+ + +
+
+ + +

Example block-level help text here.

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

Buttons +

+
+

+ + + + + + + +

+

+ + + + + + + +

+

+ + + + + + + +

+

+ + + + + + + +

+

+ + + + + + + +

+
+
+
+

Images +

+
+

+ +

+
+
+ +
+

Input Groups +

+
+
+ + +

+
+ + .00 +

+
+ $ + .00 +
+
+
+ + +
+

Pagination +

+
+ + + +
+
+
+

Labels and Badges +

+
+

Default Success  + Warning Danger Info

+

Inbox 42

+
+
+
+

Alerts +

+
+
+
+ + Oh snap! Change a few things up and + try submitting again. +
+
+ + Well done! You successfully read this important alert message. +
+
+ + Heads up! This alert needs your attention, + but it's not super important. +
+
+ + Heads up! This alert needs your attention, + but it's not super important. +
+
+ +

Warning!

+

This is a block style alert.

+
+
+
+
+
+

Progress Bars +

+
+
+
60% Complete
+
+
+
40% Complete (success)
+
+
+
20% Complete
+
+
+
60% Complete (warning)
+
+
+
80% Complete (danger)
+
+
+
35% Complete (success)
+
20% Complete (warning)
+
10% Complete (danger)
+
+
+
+
+

Media Object +

+
+

+
+ +
+

Media heading

+

This is the content for your media.

+
+ +
+

Media heading

+

This is the content for your media.

+
+
+
+
+
+
+
+

List Group +

+ +
+
+

Panels +

+
+
+
+
+
This is a header +
+

This is a panel

+ +
+
+
+
+
This is a header +
+
This is a panel
+ +
+
+
+
+
This is a header +
+
This is a panel
+ +
+
+
+
+
This is a header +
+
This is a panel
+ +
+
+
+
+
+
+
This is a header +
+

This is a panel

+ +
+
+
+
+
This is a header +
+
This is a panel
+
    +
  • First Item
  • +
  • Second Item
  • +
  • Third Item
  • +
+ +
+
+
+
+
+
+

Wells +

+
+
+
Default Well +
+
Small Well +
+
+
+
Large Padding Well +
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/styles/components/_account.less b/src/app/styles/components/_account.less new file mode 100644 index 00000000..fe838892 --- /dev/null +++ b/src/app/styles/components/_account.less @@ -0,0 +1,3 @@ +.account-buttons { + margin-bottom: 10px; +} \ No newline at end of file diff --git a/src/app/styles/components/_buyer-select.less b/src/app/styles/components/_buyer-select.less new file mode 100644 index 00000000..b4030d63 --- /dev/null +++ b/src/app/styles/components/_buyer-select.less @@ -0,0 +1,22 @@ +.btn-select-buyer { + text-align:left; + margin-bottom:10px; + span { + display:block; + white-space: normal; + } +} + +.buyer-list-menu{ + padding:0; + border:0; + background-color:@body-bg; + min-width:400px; + max-width:100%; + .table-fixed-header { + margin-bottom:0; + } + .table-container { + max-height:calc(50vh); + } +} \ No newline at end of file diff --git a/src/app/styles/components/_catalog-card.less b/src/app/styles/components/_catalog-card.less new file mode 100644 index 00000000..73595457 --- /dev/null +++ b/src/app/styles/components/_catalog-card.less @@ -0,0 +1,23 @@ +.c-product-card { + @media (min-width: @screen-sm-min) { + // add flex display to product card wrapper + // flex the product card to fill the grid item element + .oc-flex-layout.vertical; + .oc-flex-layout.flex-auto; + &__body { + .oc-flex-layout.vertical; + .oc-flex-layout.flex-auto; + } + // flex the descrip to push icons to bottom of card + &__descrip { + .oc-flex-layout.flex-auto; + } + } +} +.c-category-card { + // add flex display to category card wrapper + @media (min-width: @screen-sm-min) { + .oc-flex-layout.vertical; + .oc-flex-layout.flex-auto; + } +} diff --git a/src/app/styles/components/_catalog-facets.less b/src/app/styles/components/_catalog-facets.less new file mode 100644 index 00000000..dd90ec58 --- /dev/null +++ b/src/app/styles/components/_catalog-facets.less @@ -0,0 +1,24 @@ +.facet-list { + margin-left: 15px; +} + +h4.facet-list { + margin-left: 0!important; + margin-right: 0!important; + border-radius: 4px 4px 4px 4px; +} + +div.facet-list label { + font-weight: normal; + color: @brand-primary; +} + +div.facet-list fieldset:last-child { + margin-bottom: @form-group-margin-bottom; +} + +.facet-list-container { + background-color: #ffffff; + border-radius: 4px 4px 4px 4px; + padding-bottom: 5px; +} \ No newline at end of file diff --git a/src/app/styles/components/_category-tree.less b/src/app/styles/components/_category-tree.less new file mode 100644 index 00000000..324a4672 --- /dev/null +++ b/src/app/styles/components/_category-tree.less @@ -0,0 +1,51 @@ +treecontrol.oc-tree { + & > ul { + padding:@grid-gutter-width/4 0; + font-family:@font-family-base; + font-size:@font-size-base; + } + li { + i { + font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration + font-size: inherit; // can't have font-size inherit on line above, so need to override + text-rendering: auto; // optimizelegibility throws things off #1094 + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + &.tree-branch-head { + position: absolute; + width: 20px; + height: 20px; + line-height:20px; + vertical-align:middle; + text-align:center; + left: 10px; + top:@grid-gutter-width/4; + } + &.tree-leaf-head { + display:none !important; + } + } + &.tree-collapsed { + & > i.tree-branch-head { + &:before { + content:@fa-var-caret-right; + } + } + } + &.tree-expanded { + & > i.tree-branch-head { + &:before { + content:@fa-var-caret-down; + } + } + } + .tree-label { + display:block; + padding: @grid-gutter-width/4 @grid-gutter-width/2; + &.tree-selected { + color:@link-color; + font-weight:bold; + } + } + } +} \ No newline at end of file diff --git a/src/app/styles/components/_header.less b/src/app/styles/components/_header.less new file mode 100644 index 00000000..b0b6dd57 --- /dev/null +++ b/src/app/styles/components/_header.less @@ -0,0 +1,18 @@ +.c-header { + position: relative; + min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode) + margin-bottom: @navbar-margin-bottom; + border: 1px solid @navbar-default-border; + //brought in from .navbar-default + background-color: @navbar-default-bg; + // Prevent floats from breaking the navbar + &:extend(.clearfix all); + &--fixed-top { + position: fixed; + right: 0; + left: 0; + top: 0; + border-width: 0 0 1px; + z-index: @zindex-navbar-fixed; + } +} diff --git a/src/app/styles/components/_lineitems.less b/src/app/styles/components/_lineitems.less new file mode 100644 index 00000000..0212a303 --- /dev/null +++ b/src/app/styles/components/_lineitems.less @@ -0,0 +1,3 @@ +.modal_buttons { + margin-bottom: 50px; +} \ No newline at end of file diff --git a/src/app/styles/components/_loading-indicators.less b/src/app/styles/components/_loading-indicators.less new file mode 100644 index 00000000..859ab66d --- /dev/null +++ b/src/app/styles/components/_loading-indicators.less @@ -0,0 +1,66 @@ +.cg-busy-backdrop { + background-color:@body-bg; +} + +.indicator-container { //fade the indicator in and out + position:absolute; + left:0; right:0; top:0; bottom:0; + z-index:1001; + transform:none !important; + &.ng-hide-add, + &.ng-hide-remove { + transition:all .3s ease; + display: block !important; + } + &.ng-hide-remove, + &.ng-hide-add.ng-hide-add-active { + opacity:0; + } + &.ng-hide-add-active, + &.ng-hide-remove.ng-hide-remove-active { + opacity:1; + } +} + +.indicator { + display:flex; + flex-flow:column nowrap; + align-items:center; + justify-content:center; + position:absolute; + left:0; right:0; top:0; bottom:0; + .animation { + .dot { + display:inline-block; + margin:0 2px; + width: 15px; + height: 15px; + background-color:@brand-primary; + border-radius: 2px; + animation-name:indicator-fade-in-out; + animation-iteration-count: infinite; + animation-duration:500ms; + &:nth-of-type(2) { + animation-delay:100ms; + } + &:nth-of-type(3) { + animation-delay:200ms; + } + } + } + .message { + margin-top:15px; + } +} + +@keyframes indicator-fade-in-out { + 0% { + opacity:1; + } + 50% { + opacity:0.2; + } + 100% { + opacity:1; + } +} \ No newline at end of file diff --git a/src/app/styles/components/_minicart.less b/src/app/styles/components/_minicart.less new file mode 100644 index 00000000..ac3f27b0 --- /dev/null +++ b/src/app/styles/components/_minicart.less @@ -0,0 +1,60 @@ +// from base.less +.base-top { + #minicart { + &:extend(.navbar-right); + &:extend(.navbar-form); + margin-right:0; + margin-top:0; + margin-bottom:0; + } +} +// end from base.less + +// from cart.less +.ordercloud-minicart { + flex: 1 0 auto; +} +// end from cart.less + +#minicart { + .btn-group { + .btn-default { + font-weight: bold; + } + } + .dropdown-menu{ + width: 250px; + } + + .express-checkout { + margin-top: @form-group-margin-bottom; + } + + .badge { + background-color:@brand-primary; + } + + #minicartButton{ + padding: 15px 0 15px 15px; + @media(min-width:@screen-sm-min) { + padding-left: 0; + } + } + + #minicart_line_item_list { + padding: 0; + right: 15px; + left: auto; + .minicart-body { + max-height: 30vh; + overflow-y: auto; + overflow-x: hidden; + table { + margin-bottom: 0; + tr, td { + vertical-align: middle; + } + } + } + } +} \ No newline at end of file diff --git a/src/app/styles/components/_modal-minicart.less b/src/app/styles/components/_modal-minicart.less new file mode 100644 index 00000000..4539ae07 --- /dev/null +++ b/src/app/styles/components/_modal-minicart.less @@ -0,0 +1,7 @@ +#modalMinicart{ + .modal-footer{ + .express-checkout{ + padding-top: 15px; + } + } +} \ No newline at end of file diff --git a/src/app/styles/components/_modals.less b/src/app/styles/components/_modals.less new file mode 100644 index 00000000..715faadd --- /dev/null +++ b/src/app/styles/components/_modals.less @@ -0,0 +1,13 @@ +//full screen modal +.modal-dialog { + &.modal--full-screen { + margin: 0; + width: 100%; + .modal-content { + border-radius: 0; + border: none; + padding: 0; + height: 100vh; + } + } +} diff --git a/src/app/styles/components/_product-browse.less b/src/app/styles/components/_product-browse.less new file mode 100644 index 00000000..60dedc97 --- /dev/null +++ b/src/app/styles/components/_product-browse.less @@ -0,0 +1,7 @@ +.category-modal { + a { + max-width: 400px; + float: inherit; + margin: 0 auto; + } +} \ No newline at end of file diff --git a/src/app/styles/components/_product-search.less b/src/app/styles/components/_product-search.less new file mode 100644 index 00000000..060ac6ea --- /dev/null +++ b/src/app/styles/components/_product-search.less @@ -0,0 +1,12 @@ +// product search +.c-productsearch { + position: relative; + .form-group { + margin: 0; + } + li.active { + small { + color: white; + } + } +} diff --git a/src/app/styles/components/_quantity-input.less b/src/app/styles/components/_quantity-input.less new file mode 100644 index 00000000..54fb1239 --- /dev/null +++ b/src/app/styles/components/_quantity-input.less @@ -0,0 +1,6 @@ +#QuantityInput { + select { + text-align-last: center; + text-align:center; + } +} diff --git a/src/app/styles/components/_shipping.less b/src/app/styles/components/_shipping.less new file mode 100644 index 00000000..f49cd673 --- /dev/null +++ b/src/app/styles/components/_shipping.less @@ -0,0 +1,4 @@ +#address_paragraph { + white-space: pre; + padding-top: 10px; +} \ No newline at end of file diff --git a/src/app/styles/components/_sidebar.less b/src/app/styles/components/_sidebar.less new file mode 100644 index 00000000..83573592 --- /dev/null +++ b/src/app/styles/components/_sidebar.less @@ -0,0 +1,31 @@ +.c-sidebar { + display: none; + @media (min-width: @screen-sm-min) { + display: block; + overflow-y: auto; + height: calc(~"(100vh - @{body-padding-top})"); + } + // overwrite bootstrap specificity + &.nav { + > li { + a { + padding-top: (@grid-gutter-width / 4); + padding-bottom: (@grid-gutter-width / 4); + &:hover, + &:focus { + text-decoration: underline; + background-color: transparent; + } + } + } + } +} + +.c-section { + padding-top: @body-padding-top; + &__header { + margin-top: 0; + &:extend(.page-header); + } + &__body {} +} \ No newline at end of file diff --git a/src/app/styles/components/_tables.less b/src/app/styles/components/_tables.less new file mode 100644 index 00000000..338f76df --- /dev/null +++ b/src/app/styles/components/_tables.less @@ -0,0 +1,245 @@ +//TABLES WITH FIXED HEADERS +.table-fixed-header { + position:relative; + padding-top:@table-header-height; + margin-bottom:@line-height-computed; + overflow:hidden; + .table-header-bg { + position:absolute; + left:0; right:0; top:0; + height:@table-header-height; + background:@table-bg; + border:1px solid @table-border-color; + border-bottom-width:2px; + } + .table-container { + background-color:@table-bg; + overflow-y:auto; + overflow-x:hidden; + max-height:@infinite-scroll-size; + border: 1px solid @table-border-color; + border-top-width:0; + table { + border-spacing:0; + margin-bottom:0; + } + td { + border:1px solid @table-border-color; + vertical-align:middle; + &:first-child { + border-left:none; + } + &:last-child { + border-left:none; + } + } + th { + height: 0; + line-height: 0; + padding: 0 25px; + color: transparent; + border: none; + white-space: nowrap; + span { + max-height:0; + height:0; + line-height:0; + margin:0; + padding:0; + } + div{ + position: absolute; + background: transparent; + padding:@table-cell-padding; + color: @text-color; + top: 0; + margin-left: -25px; + line-height: @line-height-base; + vertical-align: bottom; + border-left:1px solid @table-border-color; + } + &:first-child div{ + border: none; + } + } + tbody tr { + &:first-child { + td { + border-top:none; + } + } + &:last-child { + td { + border-bottom:none; + } + } + } + } +} +colgroup { + > col { + .action-column { + width:70px; + } + } +} + +.table > tbody > tr { + > td { + &.actions-cell { + text-align:center; + white-space: nowrap; + @media(min-width:@screen-sm-min) { + text-align: right; + width: 1%; + } + } + } +} + +// No matches message +.no-matches { + @media(min-width:@screen-sm-min) { + margin-top:15px; + } + &:extend(.well); + text-align:center; +} + +// OC Responsive Tables +// commandeered from http://codepen.io/pixelchar/pen/rfuqK +.oc-table-responsive { + &.table-bordered { + border:none; + & > tbody > tr{ + & > th { + border:none; + } + & > td { + border-width:1px 0 0 0; + } + } + } + + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + vertical-align: middle; + } + } + } + + thead { + // Accessibly hide on narrow viewports + position: absolute; + clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; + } + + // Set these items to display: block for narrow viewports + tbody, + tr, + th, + td { + display: block; + white-space: normal; + } + + tr { + &:extend(.panel); //Mobile table row looks like a bootstrap panel + border-color: @panel-default-border; + overflow:hidden; + } + + tbody { + & > tr > th { + border-top: none; + background-color: @panel-default-heading-bg; + } + td { + &[data-title] { + .clearfix(); //Clearfix solution for empty table cells on mobile + &:before { + content: attr(data-title); + float: left; + color: @gray-light; + } + } + @media(max-width:@screen-sm-min) { + text-align:right; + } + } + } + + @media(min-width:@screen-sm-min) { + // Condensed tables on desktop + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: @table-condensed-cell-padding; + } + } + } + &.table-bordered { + border:1px solid @table-border-color; + & > tbody > tr{ + & > th { + border:1px solid @table-border-color; + } + & > td { + border-width:1px; + } + } + } + thead { + position: relative; + clip: auto; + height: auto; + width: auto; + overflow: auto; + } + tr { + display: table-row; + margin-bottom: 0; + border: none; + background-color:transparent; + } + th, + td { + // Undo display: block + display: table-cell; + } + tbody { + display:table-row-group; + tr { + display:table-row; + } + & > tr > th { + border-top: 1px solid @table-border-color; + background-color: transparent; + } + td { + &[data-title]:before { + content: none; + } + } + } + } + .fa-circle { + color:@gray-light; + &.active { + color:@brand-success; + } + } +} \ No newline at end of file diff --git a/src/app/styles/components/_topnav.less b/src/app/styles/components/_topnav.less new file mode 100644 index 00000000..1f0abd84 --- /dev/null +++ b/src/app/styles/components/_topnav.less @@ -0,0 +1,50 @@ +.c-topnav { + .oc-flex-layout.horizontal; + .oc-flex-layout.center; + .oc-flex-layout.wrap; + .oc-flex-layout.between-justified; + .container>& { + margin-left: -@navbar-padding-horizontal; + margin-right: -@navbar-padding-horizontal; + } + &__item { + display: inline-block; + padding: @navbar-padding-vertical @navbar-padding-horizontal; + line-height: @line-height-computed; + a { + text-decoration: none; + color: @navbar-default-link-color; + } + } + &__brand { + order: 0; + font-family: @headings-font-family; + font-size: 1.2em; + font-weight: bold; + height: @navbar-height; + a { + color: @navbar-default-brand-color; + } + &:hover, + &:focus { + text-decoration: none; + } + >img { + display: block; + } + @media(max-width:@screen-xs-max) {} + } + &__search { + @media (min-width: @screen-md-min) { + .oc-flex-layout.flex-auto; + padding-top: @navbar-padding-vertical - 7px; + padding-bottom: @navbar-padding-vertical - 7px; + @shadow: inset 0 1px 0 rgba(255, 255, 255, .1), + 0 1px 0 rgba(255, 255, 255, .1); + .box-shadow(@shadow); + } + } + &__cart { + position: relative; + } +} \ No newline at end of file diff --git a/src/app/styles/components/_tree.less b/src/app/styles/components/_tree.less new file mode 100644 index 00000000..2ca7449b --- /dev/null +++ b/src/app/styles/components/_tree.less @@ -0,0 +1,60 @@ +.category-tree { + ul { + list-style:none; + padding-left:15px; + } +} +.category-tree-item { + a { + text-decoration:none; + color:@text-color; + } + &.selected > a { + font-weight:bold; + color:@brand-primary; + } +} + +.tree-node { + background: @well-bg; + border: 1px solid @well-border; + color: @text-color; + border-radius:@border-radius-base; + &.inactive-node { + background-color: @state-warning-bg; + border-color: @state-warning-border; + } +} + +.tree-node-content { + padding:0.3em; + margin-bottom: 10px; + display:flex; + flex-flow:row nowrap; + justify-content: flex-start; + align-items: center; + p:first-child { + padding-left:10px; + } + p { + flex-grow: 1; + margin:0; + } + .fa-folder, + .fa-folder-open { + color:@link-color; + padding:10px; + width:30px; + text-align:center; + } +} + +.angular-ui-tree-placeholder { + border-radius:@border-radius-base; + background: fade(@link-color, 15) ; + border: 2px dashed @link-color; +} + +.new-category { + cursor: pointer; +} \ No newline at end of file diff --git a/src/app/styles/components/_typeahead.less b/src/app/styles/components/_typeahead.less new file mode 100644 index 00000000..511be254 --- /dev/null +++ b/src/app/styles/components/_typeahead.less @@ -0,0 +1,74 @@ +// typeahead block +.c-typeahead { + // typeahead results element + &__results { + // display results + .dropdown-menu { + right: 0; + } + // display empty and loading modifier states in a dropdown UI to match typeahead results + &--empty, + &--loading { + &:extend(.dropdown-menu); + display: block; + right: 0; + span { + &:extend(.dropdown-menu > li > a); + padding-top: 10px; + padding-bottom: 10px; + } + } + // loading modifer state + &--loading { + height: 50px; + & ~ .dropdown-menu { + display:none !important; + } + } + } +} + +// full screen modal with typeahead +.modal--full-screen { + .modal-body { + padding: 0; + } + .c-typeahead__form { + padding: @grid-gutter-width @grid-gutter-width (@grid-gutter-width / 2); + } + .c-typeahead__results { + // the dropdown menu provided by uib-typeahead + // overriding bootstraps' default dropdown menu styles + .dropdown-menu { + padding: 0; + margin: 0; + position: relative; + top: 0 !important; + left: 0 !important; + width: 100%; + border-radius: 0; + box-shadow: none; + border: none; + li { + border-bottom: 1px solid @hr-border; + a { + padding-left: @grid-gutter-width; + padding-right: @grid-gutter-width; + } + &.active { + small { + color: white; + } + } + } + } + // display empty and loading modifier states in a dropdown UI that matches typeahead results + &--empty, + &--loading { + &:extend(.modal--full-screen .c-typeahead__results .dropdown-menu); + span { + &:extend(.modal--full-screen .c-typeahead__results .dropdown-menu li a); + } + } + } +} diff --git a/src/app/styles/components/glob.less b/src/app/styles/components/glob.less new file mode 100644 index 00000000..9ce33069 --- /dev/null +++ b/src/app/styles/components/glob.less @@ -0,0 +1,20 @@ +@import '_account'; +@import '_buyer-select'; +@import '_catalog-card'; +@import '_catalog-facets'; +@import '_category-tree'; +@import '_header'; +@import '_lineitems'; +@import '_loading-indicators'; +@import '_minicart'; +@import '_modal-minicart'; +@import '_modals'; +@import '_product-browse'; +@import '_product-search'; +@import '_quantity-input'; +@import '_shipping'; +@import '_sidebar'; +@import '_tables'; +@import '_topnav'; +@import '_tree'; +@import '_typeahead'; diff --git a/src/app/styles/global/_scaffolding.less b/src/app/styles/global/_scaffolding.less new file mode 100644 index 00000000..f2081896 --- /dev/null +++ b/src/app/styles/global/_scaffolding.less @@ -0,0 +1,4 @@ +body { + // padding:(@navbar-height + 21) 0 40px; + // overflow-y:auto; +} diff --git a/src/app/styles/global/glob.less b/src/app/styles/global/glob.less new file mode 100644 index 00000000..4ea2ef3e --- /dev/null +++ b/src/app/styles/global/glob.less @@ -0,0 +1 @@ +@import '_scaffolding'; \ No newline at end of file diff --git a/src/app/styles/layout/_base.less b/src/app/styles/layout/_base.less new file mode 100644 index 00000000..e8c20236 --- /dev/null +++ b/src/app/styles/layout/_base.less @@ -0,0 +1,4 @@ +// add padding to offset body from fixed navbar +body { + padding-top: @body-padding-top; +} \ No newline at end of file diff --git a/src/app/styles/layout/_catalog-grid.less b/src/app/styles/layout/_catalog-grid.less new file mode 100644 index 00000000..4ebd55bd --- /dev/null +++ b/src/app/styles/layout/_catalog-grid.less @@ -0,0 +1,24 @@ +.l-product-grid { + // use flexbox for layout on small+ screens + // add flex display to product grid wrapper + @media (min-width: @screen-sm-min) { + .oc-flex-layout.horizontal; + .oc-flex-layout.wrap; + // flex each item within the grid wrapper + &__item { + .oc-flex-layout.vertical; + } + } +} +.l-category-grid { + // use flexbox for layout on small+ screens + // add flex display to product grid wrapper + @media (min-width: @screen-sm-min) { + .oc-flex-layout.horizontal; + .oc-flex-layout.wrap; + // flex each item within the grid wrapper + &__item { + .oc-flex-layout.vertical; + } + } +} diff --git a/src/app/styles/layout/glob.less b/src/app/styles/layout/glob.less new file mode 100644 index 00000000..05b960f0 --- /dev/null +++ b/src/app/styles/layout/glob.less @@ -0,0 +1,2 @@ +@import '_base'; +@import '_catalog-grid'; diff --git a/src/app/styles/main.less b/src/app/styles/main.less new file mode 100644 index 00000000..3ae1a2b0 --- /dev/null +++ b/src/app/styles/main.less @@ -0,0 +1,14 @@ +//Style Architecture POC + //https://scotch.io/tutorials/aesthetic-sass-1-architecture-and-style-organization +@import 'components/glob'; +@import 'global/glob'; +@import 'layout/glob'; +@import 'pages/glob'; +@import 'utils/glob'; + +//Theme styles are imported last for proper overwriting +@import 'theme/glob'; + +//shame.less is where we put all the styles that need to be organized + // and refactored into the standard architecture +@import 'shame'; \ No newline at end of file diff --git a/src/app/styles/pages/_cart.less b/src/app/styles/pages/_cart.less new file mode 100644 index 00000000..c21e2649 --- /dev/null +++ b/src/app/styles/pages/_cart.less @@ -0,0 +1,57 @@ +.c-line-item { + // use flexbox for layout on small+ screens + @media (min-width: @screen-sm-min) { + .oc-flex-layout.horizontal; + .oc-flex-layout.wrap; + .oc-flex-layout.center; + } + &__header { + h6 { + margin-top: 0; + margin-bottom: 0; + } + } + &--centered { + @media (min-width: @screen-sm-min) { + .oc-flex-layout.horizontal; + .oc-flex-layout.wrap; + .oc-flex-layout.center; + } + } + &__img { + margin-bottom: 0; + } + &__info--top { + padding-bottom: @padding-large-vertical; + @media (min-width: @screen-sm-min) { + padding-bottom: 0; + } + } + &__name { + margin-top: 0; + } + &__price { + margin-top: 0; + margin-bottom: 0; + } + &__qty-input { + width: 100%; + .form-group { + margin-bottom: 0; + } + } + &__total { + margin-top: 0; + margin-bottom: 0; + } + // declare smaller gutters on cart line items + &.row, + .row { + margin-left: -(@grid-gutter-width / 4); + margin-right: -(@grid-gutter-width / 4); + } + [class^="col-"] { + padding-left: @grid-gutter-width / 4; + padding-right: @grid-gutter-width / 4; + } +} diff --git a/src/app/styles/pages/_catalog.less b/src/app/styles/pages/_catalog.less new file mode 100644 index 00000000..2395c3b3 --- /dev/null +++ b/src/app/styles/pages/_catalog.less @@ -0,0 +1,64 @@ +#catalog-wrapper { + padding-left: 0; + transition: all 0.5s ease; + &.toggled { + padding-left: @aside-tree-width; + #catalog-aside { + width: @aside-tree-width; + } + #catalog-view { + position: absolute; + margin-right: -@aside-tree-width; + } + } +} + +#catalog-view { + > button { + margin-bottom: 5px; + } +} + +#catalog-aside { + z-index: 999; + position: fixed; + width: 0; + height: 100%; + margin-left: -@aside-tree-width; + margin-top: -20px; + overflow-y: auto; + overflow-x:hidden; + transition: all 0.5s ease; + padding: 15px; +} + +#catalog-view { + width: 100%; + padding-left: 30px; + position: absolute; +} + +@media(min-width:768px) { + #catalog-wrapper { + padding-left: @aside-tree-width; + &.toggled { + padding-left: 0; + #catalog-aside { + width: 0; + } + #catalog-view { + position: relative; + margin-right: 0; + } + } + } + + #catalog-aside { + width: @aside-tree-width; + } + + #catalog-view { + position: relative; + } +} + diff --git a/src/app/styles/pages/_category-list.less b/src/app/styles/pages/_category-list.less new file mode 100644 index 00000000..f7000179 --- /dev/null +++ b/src/app/styles/pages/_category-list.less @@ -0,0 +1,69 @@ +.product-list { + > div { + padding: 5px 10px; + .product-item { + height: 400px; + display: flex; + flex-flow: column; + align-items: center; + //border: solid black 1px; + padding: 10px; + //box-shadow: 10px 10px 15px -5px rgba(0,0,0,0.75); + .thumbnail-image { + display: flex; + align-items: center; + flex-grow: 1; + img { + max-width: 100%; + max-height: 200px; + } + } + } + .product-item:hover { + //box-shadow: 10px 10px 10px -5px rgba(0,0,0,0.75); + } + .item-description { + margin: 10px 0 0; + max-height: 75px; + overflow: hidden; + p { + width: 100%; + text-overflow: ellipsis; + } + } + } +} + +#catalog-tree { + ul { + margin-left : 20px; + } + > ul { + margin-left: 0px; + } +} + +.category-list { + display:flex; + flex-flow:row wrap; + justify-content:center; + margin:0 -15px; + .category-list-item { + flex:0 1 auto; + padding:0 15px; + } + figure { + overflow:hidden; + background-color:@table-bg-accent; + } + img { + margin:0 auto; + } + h4 { + margin:0 auto; + max-width:300px; + overflow:hidden; + white-space: nowrap; + text-overflow: ellipsis; + } +} \ No newline at end of file diff --git a/src/app/styles/pages/_checkout.less b/src/app/styles/pages/_checkout.less new file mode 100644 index 00000000..967add40 --- /dev/null +++ b/src/app/styles/pages/_checkout.less @@ -0,0 +1,35 @@ +#checkout_nav { + padding-bottom: 20px; +} + +#LineItems { + .table { + margin-bottom: 0; + .item_info { + border-bottom: none; + } + .shipping_info { + td { + border-top: none; + } + border-top: none; + } + } + tr{ + &:first-child { + > td { + border-top:none; + } + } + } +} + +@media print { + main { + margin-left: -250px!important; + } +} + +.add-payment { + margin-top: 10px; +} \ No newline at end of file diff --git a/src/app/styles/pages/_home.less b/src/app/styles/pages/_home.less new file mode 100644 index 00000000..874be788 --- /dev/null +++ b/src/app/styles/pages/_home.less @@ -0,0 +1,3 @@ +#COMPONENT_Home { + padding-top:15px; +} \ No newline at end of file diff --git a/src/app/styles/pages/_login.less b/src/app/styles/pages/_login.less new file mode 100644 index 00000000..5447b4e9 --- /dev/null +++ b/src/app/styles/pages/_login.less @@ -0,0 +1,11 @@ +.login { + position:fixed; + left:0; + right:0; + bottom:0; + top:0; + .panel { + max-width:300px; + margin:0 auto; + } +} \ No newline at end of file diff --git a/src/app/styles/pages/_my-orders.less b/src/app/styles/pages/_my-orders.less new file mode 100644 index 00000000..0a57183f --- /dev/null +++ b/src/app/styles/pages/_my-orders.less @@ -0,0 +1,3 @@ +.payment-item { + height: 115px; +} \ No newline at end of file diff --git a/src/app/styles/pages/glob.less b/src/app/styles/pages/glob.less new file mode 100644 index 00000000..bf9df75f --- /dev/null +++ b/src/app/styles/pages/glob.less @@ -0,0 +1,7 @@ +@import '_cart'; +@import '_catalog'; +@import '_category-list'; +@import '_checkout'; +@import '_home'; +@import '_login'; +@import '_my-orders'; \ No newline at end of file diff --git a/src/app/styles/shame.less b/src/app/styles/shame.less new file mode 100644 index 00000000..8c997e97 --- /dev/null +++ b/src/app/styles/shame.less @@ -0,0 +1,12 @@ +[ui-sref], +[ng-click]{ + cursor: pointer; +} + +body.modal-open { + padding-right:15px !important; +} + +a[disabled] { + pointer-events: none; +} \ No newline at end of file diff --git a/src/app/styles/theme/_oc-darkness.less b/src/app/styles/theme/_oc-darkness.less new file mode 100644 index 00000000..ec09a86d --- /dev/null +++ b/src/app/styles/theme/_oc-darkness.less @@ -0,0 +1,141 @@ +// +// OrderCloud Dark UI +// -------------------------------------------------- + +//== Colors +@gray-base: desaturate(shade(@brand-secondary,86%),65%); +@gray-darker: tint(@gray-base, 10%); +@gray-dark: tint(@gray-base, 15%); +@gray: tint(@gray-base, 50%); +@gray-light: tint(@gray-base, 85%); +@gray-lighter: tint(@gray-base, 90%); + +@brand-primary: #008FDC; //OrderCloud Blue +@brand-secondary: @brand-primary; + +@brand-success: #5cb85c; +@brand-info: #51CEF6; +@brand-warning: darken(#FBD764,15); +@brand-danger: #E13D00; + +//== Scaffolding +@body-bg: @gray-base; +@text-color: @gray-lighter; +@link-hover-color: tint(@link-color, 15%); + +//== Tables +@table-bg-accent: @gray-dark; +@table-bg-hover: @gray-dark; +@table-border-color: @gray-dark; + +//== Buttons +@btn-default-color: @gray-lighter; +@btn-default-bg: @gray-dark; +@btn-default-border: @gray-darker; + +//== Forms +@legend-color: @gray-light; +@legend-border-color: @gray-light; + +//== Dropdowns +@dropdown-bg: @body-bg; +@dropdown-border: @gray-dark; +@dropdown-divider-bg: @gray-dark; +@dropdown-link-color: @gray-lighter; +@dropdown-link-hover-color: darken(@gray-light, 5%); +@dropdown-link-hover-bg: @gray-dark; +@dropdown-link-disabled-color: @gray; +@dropdown-header-color: @gray-light; + +//== Navbar +@navbar-default-color: @gray-lighter; +@navbar-default-bg: @gray-base; +@navbar-default-border: @gray-dark; +@navbar-default-link-color: @gray-light; +@navbar-default-link-hover-color: @gray-lighter; +@navbar-default-link-active-color: @gray-lighter; +@navbar-default-brand-hover-color: @navbar-default-link-hover-color; + +//=== Inverted navbar +@navbar-inverse-color: @gray-darker; +@navbar-inverse-bg: @gray-lighter; +@navbar-inverse-border: @gray-dark; +@navbar-inverse-link-color: @gray-darker; +@navbar-inverse-link-hover-color: @gray-dark; +@navbar-inverse-link-active-bg: @gray-light; +@navbar-inverse-brand-color: @navbar-inverse-link-color; +@navbar-inverse-brand-hover-color: @gray-dark; + +//== Navs +@nav-link-hover-bg: @gray-darker; +@nav-tabs-border-color: @gray-dark; +@nav-tabs-link-hover-border-color: @gray-dark; +@nav-tabs-active-link-hover-bg: @gray-darker; +@nav-tabs-active-link-hover-border-color: @gray-dark; + +//== Pagination +@pagination-bg: @body-bg; +@pagination-border: @gray-darker; +@pagination-hover-bg: @gray-darker; +@pagination-hover-border: @gray-darker; +@pagination-active-border: @gray-darker; +@pagination-disabled-color: @gray-darker; +@pagination-disabled-bg: @body-bg; +@pagination-disabled-border: @gray-darker; + +//== Jumbotron +@jumbotron-bg: @gray-darker; + +//== Form states and alerts +@state-success-text: lighten(@brand-success,60); +@state-success-bg: darken(@brand-success,5); +@state-success-border: darken(@state-success-bg, 5%); +@state-info-text: lighten(@brand-info,60); +@state-info-bg: darken(@brand-info,15); +@state-info-border: darken(@state-info-bg, 7%); +@state-warning-text: lighten(@brand-warning,60); +@state-warning-bg: darken(@brand-warning,5); +@state-warning-border: darken(@state-warning-bg, 5%); +@state-danger-text: lighten(@brand-danger,60); +@state-danger-bg: darken(@brand-danger,5); +@state-danger-border: darken(@state-danger-bg, 5%); + +//== Labels +@label-default-bg: @gray; + +//== Modals +@modal-content-bg: @gray-darker; +@modal-content-border-color: @gray-darker; +@modal-header-border-color: @gray-dark; + +//== List group +@list-group-bg: @body-bg; +@list-group-border: @gray-darker; +@list-group-hover-bg: @gray-darker; +@list-group-disabled-color: @gray-dark; +@list-group-disabled-bg: @gray-darker; +@list-group-link-color: @gray-light; +@list-group-link-heading-color: @gray-lighter; + +//== Panels +@panel-bg: @gray-darker; +@panel-inner-border: @gray-dark; +@panel-footer-bg: @gray-darker; +@panel-default-text: @gray-lighter; +@panel-default-border: @gray-dark; +@panel-default-heading-bg: @gray-dark; + +//== Thumbnails +@thumbnail-border: @gray-dark; + +//== Wells +@well-bg: @gray-dark; +@well-border: @gray-darker; + +//== Badges +@badge-bg: @brand-primary; + +//== Type +@text-muted: @gray; +@headings-small-color: @gray; +@blockquote-small-color: @gray; diff --git a/src/app/styles/theme/_oc-lightness.less b/src/app/styles/theme/_oc-lightness.less new file mode 100644 index 00000000..623f726d --- /dev/null +++ b/src/app/styles/theme/_oc-lightness.less @@ -0,0 +1,141 @@ +// +// OrderCloud Light UI +// -------------------------------------------------- + +//== Colors +@gray-base: desaturate(shade(@brand-secondary,86%),100%); +@gray-darker: tint(@gray-base, 5%); +@gray-dark: tint(@gray-base, 10%); +@gray: tint(@gray-base, 50%); +@gray-light: tint(@gray-base, 85%); +@gray-lighter: tint(@gray-base, 95%); + +@brand-primary: #008FDC; //OrderCloud Blue +@brand-secondary: @brand-primary; + +@brand-success: #5cb85c; +@brand-info: #51CEF6; +@brand-warning: #FBD764; +@brand-danger: #E13D00; + +//== Scaffolding +@body-bg: #FFFFFF; +@text-color: @gray-dark; +@link-hover-color: darken(@link-color, 15%); + +//== Tables +@table-bg-accent: @gray-light; +@table-bg-hover: darken(@gray-lighter, 2.5%); +@table-border-color: @gray-light; + +//== Buttons +@btn-default-color: @gray-darker; +@btn-default-bg: #fff; +@btn-default-border: @gray-light; + +//== Forms +@legend-color: @gray-dark; +@legend-border-color: @gray-light; + +//== Dropdowns +@dropdown-bg: @body-bg; +@dropdown-border: @gray-light; +@dropdown-divider-bg: @gray-light; +@dropdown-link-color: @gray-darker; +@dropdown-link-hover-color: darken(@gray-dark, 5%); +@dropdown-link-hover-bg: @gray-lighter; +@dropdown-link-disabled-color: @gray; +@dropdown-header-color: @gray; + +//== Navbar +@navbar-default-color: @gray-darker; +@navbar-default-bg: @gray-lighter; +@navbar-default-border: @gray-light; +@navbar-default-link-color: @gray-dark; +@navbar-default-link-hover-color: @gray-darker; +@navbar-default-link-active-color: @gray-darker; +@navbar-default-brand-hover-color: @navbar-default-link-hover-color; + +//=== Inverted navbar +@navbar-inverse-color: @gray-lighter; +@navbar-inverse-bg: @gray-darker; +@navbar-inverse-border: @gray-light; +@navbar-inverse-link-color: @gray-lighter; +@navbar-inverse-link-hover-color: @gray-light; +@navbar-inverse-link-active-bg: @gray-dark; +@navbar-inverse-brand-color: @navbar-inverse-link-color; +@navbar-inverse-brand-hover-color: @gray-light; + +//== Navs +@nav-link-hover-bg: @gray-lighter; +@nav-tabs-border-color: @gray-light; +@nav-tabs-link-hover-border-color: @gray-light; +@nav-tabs-active-link-hover-bg: @gray-lighter; +@nav-tabs-active-link-hover-border-color: @gray-light; + +//== Pagination +@pagination-bg: @body-bg; +@pagination-border: @gray-light; +@pagination-hover-bg: @gray-lighter; +@pagination-hover-border: @gray-lighter; +@pagination-active-border: @gray-lighter; +@pagination-disabled-color: @gray; +@pagination-disabled-bg: @body-bg; +@pagination-disabled-border: @gray-light; + +//== Jumbotron +@jumbotron-bg: @gray-lighter; + +//== Form states and alerts +@state-success-text: #3c763d; +@state-success-bg: #dff0d8; +@state-success-border: darken(spin(@state-success-bg, -10), 5%); +@state-info-text: #31708f; +@state-info-bg: #d9edf7; +@state-info-border: darken(spin(@state-info-bg, -10), 7%); +@state-warning-text: #8a6d3b; +@state-warning-bg: #fcf8e3; +@state-warning-border: darken(spin(@state-warning-bg, -10), 5%); +@state-danger-text: #a94442; +@state-danger-bg: #f2dede; +@state-danger-border: darken(spin(@state-danger-bg, -10), 5%); + +//== Labels +@label-default-bg: @gray; + + //== Modals +@modal-content-bg: @gray-lighter; +@modal-content-border-color: @gray-lighter; +@modal-header-border-color: @gray-light; + +//== List group +@list-group-bg: @body-bg; +@list-group-border: @gray-light; +@list-group-hover-bg: @gray-lighter; +@list-group-disabled-color: @gray-light; +@list-group-disabled-bg: @gray-lighter; +@list-group-link-color: @gray-dark; +@list-group-link-heading-color: @gray-darker; + +//== Panels +@panel-bg: @gray-lighter; +@panel-inner-border: @gray-lighter; +@panel-footer-bg: @gray-lighter; +@panel-default-text: @gray-darker; +@panel-default-border: @gray-light; +@panel-default-heading-bg: @gray-light; + +//== Thumbnails +@thumbnail-border: @gray-light; + +//== Wells +@well-bg: @gray-light; +@well-border: @gray-lighter; + +//== Badges +@badge-bg: @brand-primary; + +//== Type +@text-muted: @gray; +@headings-small-color: @gray; +@blockquote-small-color: @gray; diff --git a/src/app/styles/theme/glob.less b/src/app/styles/theme/glob.less new file mode 100644 index 00000000..5358bdbe --- /dev/null +++ b/src/app/styles/theme/glob.less @@ -0,0 +1,2 @@ +// @import '_oc-darkness'; +// @import '_oc-lightness'; diff --git a/src/app/styles/utils/_flexbox.less b/src/app/styles/utils/_flexbox.less new file mode 100644 index 00000000..c0599f4d --- /dev/null +++ b/src/app/styles/utils/_flexbox.less @@ -0,0 +1,469 @@ +.make-flex-grid(@suffix) { + &.inline@{suffix} { + display: inline-flex; + } + &.horizontal@{suffix} { + display: flex; + flex-direction: row; + &.gutter@{suffix} { + >* { + margin-right: 7.5px; + margin-left: 7.5px; + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } + } + } + } + &.vertical@{suffix} { + display: flex; + flex-direction: column; + &.gutter@{suffix} { + >* { + margin-top: 7.5px; + margin-bottom: 7.5px; + &:first-child { + margin-top: 0; + } + &:last-child { + margin-bottom: 0; + } + } + } + } + &.wrap@{suffix} { + flex-wrap: wrap; + } + &.no-wrap@{suffix} { + flex-wrap: nowrap; + } + .flex@{suffix} { + flex: 1; + flex-basis: 0.000000001px; + } + .flex-auto@{suffix} { + flex: 1 1 auto; + } + .flex-none@{suffix} { + flex: none; + } + //reverse + &.horizontal-reverse@{suffix}, + &.vertical-reverse@{suffix} { + display: flex; + } + &.horizontal-reverse@{suffix} { + flex-direction: row-reverse; + } + &.vertical-reverse@{suffix} { + flex-direction: column-reverse; + } + &.wrap-reverse@{suffix} { + flex-wrap: wrap-reverse; + } + //alignment - cross axis + &.start@{suffix} { + align-items: flex-start; + } + &.center@{suffix}, + &.center-center@{suffix} { + align-items: center; + } + &.end@{suffix} { + align-items: flex-end; + } + &.baseline@{suffix} { + align-items: baseline; + } + //alignment - main axis + &.start-justified@{suffix} { + justify-content: flex-start; + } + &.center-justified@{suffix}, + &.center-center@{suffix} { + justify-content: center; + } + &.end-justified@{suffix} { + justify-content: flex-end; + } + &.around-justified@{suffix} { + justify-content: space-around; + } + &.between-justified@{suffix} { + justify-content: space-between; + } + //self alignment + .self-start@{suffix} { + align-self: flex-start; + } + .self-center@{suffix} { + align-self: center; + } + .self-end@{suffix} { + align-self: flex-end; + } + .self-stretch@{suffix} { + align-self: stretch; + } + .self-baseline@{suffix} { + align-self: baseline; + } + //multi-line alignment - main axis + &.start-aligned@{suffix} { + align-content: flex-start; + } + &.end-aligned@{suffix} { + align-content: flex-end; + } + &.center-aligned@{suffix} { + align-content: center; + } + &.between-aligned@{suffix} { + align-content: space-between; + } + &.around-aligned@{suffix} { + justify-content: space-around; + } + // flex factors + .flex-grow@{suffix} { + flex-grow: 1; + } + .flex-shrink@{suffix} { + flex-shrink: 1; + } + .flex@{suffix}, + .flex-1@{suffix} { + flex: 1; + flex-basis: 0.000000001px; + } + .flex-2@{suffix} { + flex: 2; + } + .flex-3@{suffix} { + flex: 3; + } + .flex-4@{suffix} { + flex: 5; + } + .flex-5@{suffix} { + flex: 5; + } + .flex-6@{suffix} { + flex: 6; + } + .flex-7@{suffix} { + flex: 7; + } + .flex-8@{suffix} { + flex: 8; + } + .flex-9@{suffix} { + flex: 9; + } + .flex-10@{suffix} { + flex: 10; + } + .flex-12@{suffix} { + flex: 12; + } + .block@{suffix} { + display: block; + } + // IE 10 support for HTML5 hidden attr + [hidden] { + display: none !important; + } + .invisible@{suffix} { + visibility: hidden !important; + } + .relative@{suffix} { + position: relative; + } + .fit@{suffix} { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + body.fullbleed@{suffix} { + margin: 0; + height: 100vh; + } + .scroll@{suffix} { + -webkit-overflow-scrolling: touch; + overflow: auto; + } + // fixed position + .fixed-bottom@{suffix}, + .fixed-left@{suffix}, + .fixed-right@{suffix}, + .fixed-top@{suffix} { + position: fixed; + } + .fixed-top@{suffix} { + top: 0; + left: 0; + right: 0; + } + .fixed-right@{suffix} { + top: 0; + right: 0; + bottom: 0; + } + .fixed-bottom@{suffix} { + right: 0; + bottom: 0; + left: 0; + } + .fixed-left@{suffix} { + top: 0; + bottom: 0; + left: 0; + } +} + +.oc-flex-layout() { + display: flex; + &.inline { + display: inline-flex; + } + &.horizontal { + display: flex; + flex-direction: row; + &.gutter { + >* { + margin-right: 7.5px; + margin-left: 7.5px; + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } + } + } + } + &.vertical { + display: flex; + flex-direction: column; + &.gutter { + >* { + margin-top: 7.5px; + margin-bottom: 7.5px; + &:first-child { + margin-top: 0; + } + &:last-child { + margin-bottom: 0; + } + } + } + } + &.wrap { + flex-wrap: wrap; + } + &.no-wrap { + flex-wrap: nowrap; + } + .flex { + flex: 1; + flex-basis: 0.000000001px; + } + .flex-auto { + flex: 1 1 auto; + } + .flex-none { + flex: none; + } + //reverse + &.horizontal-reverse, + &.vertical-reverse { + display: flex; + } + &.horizontal-reverse { + flex-direction: row-reverse; + } + &.vertical-reverse { + flex-direction: column-reverse; + } + &.wrap-reverse { + flex-wrap: wrap-reverse; + } + //alignment - cross axis + &.start { + align-items: flex-start; + } + &.center, + &.center-center { + align-items: center; + } + &.end { + align-items: flex-end; + } + &.baseline { + align-items: baseline; + } + //alignment - main axis + &.start-justified { + justify-content: flex-start; + } + &.center-justified, + &.center-center { + justify-content: center; + } + &.end-justified { + justify-content: flex-end; + } + &.around-justified { + justify-content: space-around; + } + &.between-justified { + justify-content: space-between; + } + //self alignment + .self-start { + align-self: flex-start; + } + .self-center { + align-self: center; + } + .self-end { + align-self: flex-end; + } + .self-stretch { + align-self: stretch; + } + .self-baseline { + align-self: baseline; + } + //multi-line alignment - main axis + &.start-aligned { + align-content: flex-start; + } + &.end-aligned { + align-content: flex-end; + } + &.center-aligned { + align-content: center; + } + &.between-aligned { + align-content: space-between; + } + &.around-aligned { + justify-content: space-around; + } + // flex factors + .flex-grow { + flex-grow: 1; + } + .flex-shrink { + flex-shrink: 1; + } + .flex, + .flex-1 { + flex: 1; + flex-basis: 0.000000001px; + } + .flex-2 { + flex: 2; + } + .flex-3 { + flex: 3; + } + .flex-4 { + flex: 5; + } + .flex-5 { + flex: 5; + } + .flex-6 { + flex: 6; + } + .flex-7 { + flex: 7; + } + .flex-8 { + flex: 8; + } + .flex-9 { + flex: 9; + } + .flex-10 { + flex: 10; + } + .flex-12 { + flex: 12; + } + .block { + display: block; + } + /* IE 10 support for HTML5 hidden attr */ + [hidden] { + display: none !important; + } + .invisible { + visibility: hidden !important; + } + .relative { + position: relative; + } + .fit { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + body.fullbleed { + margin: 0; + height: 100vh; + } + .scroll { + -webkit-overflow-scrolling: touch; + overflow: auto; + } + /* fixed position */ + .fixed-bottom, + .fixed-left, + .fixed-right, + .fixed-top { + position: fixed; + } + .fixed-top { + top: 0; + left: 0; + right: 0; + } + .fixed-right { + top: 0; + right: 0; + bottom: 0; + } + .fixed-bottom { + right: 0; + bottom: 0; + left: 0; + } + .fixed-left { + top: 0; + bottom: 0; + left: 0; + } + .make-flex-grid(~''); + @media(max-width: @screen-xs-max) { + .make-flex-grid(-xs); + } + @media(min-width:@screen-sm-min) and (max-width:@screen-sm-max) { + .make-flex-grid(-sm); + } + @media(min-width:@screen-md-min) and (max-width:@screen-md-max) { + .make-flex-grid(-md); + } + @media(min-width:@screen-lg-min) { + .make-flex-grid(-lg); + } +} \ No newline at end of file diff --git a/src/app/styles/utils/_mixins.less b/src/app/styles/utils/_mixins.less new file mode 100644 index 00000000..5a026f28 --- /dev/null +++ b/src/app/styles/utils/_mixins.less @@ -0,0 +1,13 @@ +.clearfix() { + &:before, + &:after { + content: " "; // 1 + display: table; // 2 + // adding width and height 0 to fix safari flexbox layout bug + width: 0; + height: 0; + } + &:after { + clear: both; + } +} \ No newline at end of file diff --git a/src/app/styles/utils/_variables.less b/src/app/styles/utils/_variables.less new file mode 100644 index 00000000..1b228970 --- /dev/null +++ b/src/app/styles/utils/_variables.less @@ -0,0 +1,15 @@ +//These are the variables used throughout the application. This is where +//overwrites that are not specific to components should be maintained. +//Interconnected table color styles + +//Table Infinite Scroll +@table-header-height:@table-cell-padding*2 + @line-height-computed; +@infinite-scroll-size: ~'calc(100vh - 420px)'; + +//Global Component Styles +@global-component-bg:@body-bg; + +@body-padding-top: @navbar-height + @navbar-margin-bottom; + +// from catalog/less +@aside-tree-width: 350px; \ No newline at end of file diff --git a/src/app/styles/utils/glob.less b/src/app/styles/utils/glob.less new file mode 100644 index 00000000..fa5d1c48 --- /dev/null +++ b/src/app/styles/utils/glob.less @@ -0,0 +1,3 @@ +@import '_flexbox'; +@import '_mixins'; +@import '_variables'; diff --git a/src/assets/images/favicon.png b/src/assets/images/favicon.png new file mode 100644 index 00000000..46b625fb Binary files /dev/null and b/src/assets/images/favicon.png differ diff --git a/src/index.html b/src/index.html index c185acc8..0f0aba9a 100644 --- a/src/index.html +++ b/src/index.html @@ -4,17 +4,39 @@ +<<<<<<< HEAD OrderCloud +======= + OrderCloud +>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2 +<<<<<<< HEAD +======= + + + + + +
+
+
+
+
+
+
+
+
+
+>>>>>>> 281bb9e29d0e44c929457c755c5b59714e368ee2