├── .gitignore ├── LICENSE ├── Model ├── Estimation.js ├── EstimationModel.js ├── Project.js ├── Role.js └── User.js ├── README.md ├── app.js ├── auth └── index.js ├── bin └── www ├── config ├── config.json └── index.js ├── db ├── StatusStorage.js └── createDb.js ├── error └── index.js ├── lib └── mongoose.js ├── package.json ├── public ├── favicon.ico └── images │ └── blank_account.png ├── routes ├── estimationModels.js ├── estimations.js ├── index.js ├── projects.js └── users.js ├── test.js └── views ├── .bowerrc ├── app.js ├── bower.json ├── controllers ├── MainController.js ├── estimation │ ├── EstimationAddController.js │ └── EstimationDetailController.js ├── project │ ├── ProjectAddController.js │ ├── ProjectEstimationsController.js │ └── ProjectsController.js └── user │ ├── LoginController.js │ └── UserProfileController.js ├── directives ├── confirmPopup.js ├── contenteditable.js ├── equality.js └── growl.js ├── error.html ├── error.jade ├── index.html ├── layout.jade ├── routes.js ├── services ├── $toast.js ├── AuthService.js ├── BroadcastService.js ├── Session.js ├── StatusService.js ├── StorageService.js └── TranslationService.js ├── styles ├── estimationTable.css ├── growl.min.css └── main.css ├── translations ├── translation_en.json └── translation_ru.json └── views ├── pages ├── estimation │ ├── estimation.html │ ├── estimationAdd.html │ └── estimations.html ├── index.html ├── project │ ├── projectAdd.html │ └── projects.html └── user │ ├── login.html │ └── profile.html ├── popups ├── confirmPopup.html └── toast.html └── templates ├── footer.html ├── header.html └── main.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | #frontend libs 40 | views/libs 41 | 42 | #idea 43 | .idea 44 | 45 | public user files 46 | public/users -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Model/Estimation.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('../lib/mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | 5 | var schema = new Schema({ 6 | 7 | key: String, 8 | summary: String, 9 | 10 | itsLink: String, 11 | 12 | comments: [{ 13 | text: String 14 | }], 15 | 16 | estimationModel: { 17 | fields: [String], 18 | estimationTimeNeeded: Boolean, 19 | mngmntModel: [ 20 | { 21 | name: String, 22 | percent: Number 23 | } 24 | ] 25 | }, 26 | 27 | analysis: { 28 | subSections: [ 29 | { 30 | descr : String, 31 | estimation: Number 32 | } 33 | ] 34 | }, 35 | 36 | estimationTime: Number, 37 | developmentTime: Number, 38 | totalTime: Number, 39 | component:String, 40 | version: String, 41 | 42 | approvedDate: Date, 43 | workStartDate: Date, 44 | workEndDate: Date, 45 | 46 | projectKey: String, 47 | status: { 48 | name: String, 49 | value: String, 50 | style: String 51 | }, 52 | sections: [ 53 | { 54 | number: String, 55 | name:String, 56 | subSections: [ 57 | { 58 | subNum : String, 59 | descr : String, 60 | estimation:[Number] 61 | } 62 | ] 63 | } 64 | ] 65 | }, { minimize: false }); 66 | 67 | 68 | var Estimation = mongoose.model('Estimation', schema); 69 | 70 | module.exports = Estimation; -------------------------------------------------------------------------------- /Model/EstimationModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 21-Dec-16. 3 | */ 4 | var mongoose = require('../lib/mongoose'); 5 | var Schema = mongoose.Schema; 6 | 7 | 8 | var schema = new Schema({ 9 | fieldType: String, 10 | fieldName: String, 11 | fieldKey: String 12 | }); 13 | 14 | var EstimationModel = mongoose.model('EstimationModel', schema); 15 | 16 | var FieldTypes = { 17 | String: 'String', 18 | Number: 'Number', 19 | Date: 'Date', 20 | Object: 'Object' 21 | } 22 | 23 | var preparedModel = [ 24 | { 25 | fieldType: FieldTypes.String, 26 | fieldName: "key", 27 | fieldKey: "KEY" 28 | },{ 29 | fieldType: FieldTypes.String, 30 | fieldName: "summary", 31 | fieldKey: "SUMMARY" 32 | },{ 33 | fieldType: FieldTypes.Number, 34 | fieldName: "estimationTime", 35 | fieldKey: "ESTIMATION_TIME" 36 | },{ 37 | fieldType: FieldTypes.Number, 38 | fieldName: "developmentTime", 39 | fieldKey: "DEVELOPMENT_TIME" 40 | },{ 41 | fieldType: FieldTypes.Number, 42 | fieldName: "totalTime", 43 | fieldKey: "TOTAL_TIME" 44 | },{ 45 | fieldType: FieldTypes.String, 46 | fieldName: "component", 47 | fieldKey: "COMPONENT" 48 | },{ 49 | fieldType: FieldTypes.String, 50 | fieldName: "version", 51 | fieldKey: "VERSION" 52 | },{ 53 | fieldType: FieldTypes.Date, 54 | fieldName: "approvedDate", 55 | fieldKey: "APPROVED_DATE" 56 | },{ 57 | fieldType: FieldTypes.Date, 58 | fieldName: "workStartDate", 59 | fieldKey: "WORKSTART_DATE" 60 | },{ 61 | fieldType: FieldTypes.Date, 62 | fieldName: "workEndDate", 63 | fieldKey: "WORKEND_DATE" 64 | },{ 65 | fieldType: FieldTypes.String, 66 | fieldName: "projectKey", 67 | fieldKey: "PROJECT_KEY" 68 | },{ 69 | fieldType: FieldTypes.String, 70 | fieldName: "status", 71 | fieldKey: "STATUS" 72 | } 73 | ]; 74 | 75 | module.exports.EstimationModel = EstimationModel; 76 | module.exports.preparedModel = preparedModel; -------------------------------------------------------------------------------- /Model/Project.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 13-Nov-16. 3 | */ 4 | var mongoose = require('../lib/mongoose'); 5 | var Schema = mongoose.Schema; 6 | 7 | 8 | var schema = new Schema({ 9 | name: String, 10 | key: String, 11 | itsLink: String, 12 | estimationModel: { 13 | fields: [String], 14 | estimationTimeNeeded: Boolean, 15 | mngmntModel: [ 16 | { 17 | name: String, 18 | percent: Number 19 | } 20 | ] 21 | }, 22 | 23 | versions: [String], 24 | components: [String] 25 | }); 26 | 27 | var Project = mongoose.model('Project', schema); 28 | 29 | module.exports = Project; -------------------------------------------------------------------------------- /Model/Role.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 16-Dec-16. 3 | */ 4 | var mongoose = require('../lib/mongoose'); 5 | var Schema = mongoose.Schema; 6 | var ObjectId = Schema.Types.ObjectId; 7 | 8 | var Role; 9 | 10 | var schema = new Schema({ 11 | name: String, 12 | label: String, 13 | accessLevel: Number, 14 | users: [{ type: ObjectId, ref: 'User' }] 15 | }); 16 | 17 | Role = mongoose.model('Role', schema); 18 | 19 | module.exports = Role; -------------------------------------------------------------------------------- /Model/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 15-Nov-16. 3 | */ 4 | var crypto = require('crypto'); 5 | var async = require('async'); 6 | var util = require('util'); 7 | 8 | var mongoose = require('../lib/mongoose'); 9 | var Schema = mongoose.Schema; 10 | var ObjectId = Schema.Types.ObjectId; 11 | 12 | 13 | var schema = new Schema({ 14 | 15 | name: String, 16 | avatarName: String, 17 | email: { 18 | type: String, 19 | unique: true, 20 | required: true 21 | }, 22 | 23 | login: { 24 | type: String, 25 | unique: true, 26 | required: true 27 | }, 28 | 29 | _role: { 30 | type: ObjectId, 31 | ref: 'Role' 32 | }, 33 | 34 | hashedPassword: { 35 | type: String, 36 | required: true, 37 | select: false 38 | }, 39 | 40 | salt: { 41 | type: String, 42 | required: true, 43 | select: false 44 | }, 45 | 46 | created: { 47 | type: Date, 48 | default: Date.now 49 | }, 50 | 51 | language: { 52 | key:String, 53 | value: String 54 | } 55 | }); 56 | schema.methods.encryptPassword = function(password) { 57 | if(!password) return false; 58 | return crypto.createHmac('sha1', this.salt).update(password).digest('hex'); 59 | }; 60 | 61 | schema.virtual('password') 62 | .set(function(password) { 63 | this._plainPassword = password; 64 | this.salt = Math.random() + ''; 65 | this.hashedPassword = this.encryptPassword(password); 66 | }) 67 | .get(function() { return this._plainPassword; }); 68 | 69 | 70 | schema.methods.checkPassword = function(password) { 71 | return this.encryptPassword(password) === this.hashedPassword; 72 | }; 73 | 74 | schema.statics.authorize = function(username, password, callback) { 75 | var User = this; 76 | 77 | async.waterfall([ 78 | function(callback) { 79 | User.findOne({username: username}, callback); 80 | }, 81 | function(user, callback) { 82 | if (user) { 83 | if (user.checkPassword(password)) { 84 | callback(null, user); 85 | } else { 86 | callback(new AuthError("Пароль неверен")); 87 | } 88 | } else { 89 | var user = new User({username: username, password: password}); 90 | user.save(function(err) { 91 | if (err) return callback(err); 92 | callback(null, user); 93 | }); 94 | } 95 | } 96 | ], callback); 97 | }; 98 | 99 | exports.User = mongoose.model('User', schema); 100 | 101 | function AuthError(message) { 102 | Error.apply(this, arguments); 103 | Error.captureStackTrace(this, AuthError); 104 | 105 | this.message = message; 106 | } 107 | 108 | util.inherits(AuthError, Error); 109 | 110 | AuthError.prototype.name = 'AuthError'; 111 | 112 | exports.AuthError = AuthError; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # estimator 2 | Small pet-project which helps to prepare estimates for tasks and reconcile it with customer 3 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var bodyParser = require('body-parser'); 6 | var HttpError = require('./error').HttpError; 7 | var fileupload = require('express-fileupload'); 8 | var mongoose = require('./lib/mongoose'); 9 | 10 | var app = express(); 11 | 12 | app.use(express.static(path.join(__dirname, 'views'))); 13 | app.use('/public', express.static(path.join(__dirname, 'public'))); 14 | app.use(fileupload()); 15 | app.use(require('cookie-parser')()); 16 | app.use(bodyParser.json()); 17 | app.use(bodyParser.urlencoded({extended: true})); 18 | 19 | // auth setup 20 | var session = require('express-session'); 21 | var MongoDBStore = require('connect-mongodb-session')(session); 22 | var store = new MongoDBStore( 23 | { 24 | uri: 'mongodb://localhost/estimator', 25 | collection: 'sessions' 26 | }); 27 | // Catch errors 28 | store.on('error', function (err) { 29 | console.log(err); 30 | }); 31 | 32 | var sessionOpts = { 33 | secret: 'This is a secret', 34 | cookie: { 35 | maxAge: 1000 * 60 * 60 * 24 * 7 // 1 week 36 | }, 37 | store: store, 38 | resave: true, 39 | saveUninitialized: true 40 | }; 41 | app.use(session(sessionOpts)); 42 | 43 | // view engine setup 44 | 45 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 46 | app.use(logger('dev')); 47 | 48 | app.use(function (req, res, next) { 49 | 50 | var allowedOrigins = ["http://localhost:3000", "http://estimator.senla.eu", "http://192.168.1.60"]; 51 | 52 | var origin = req.headers.origin; 53 | if(allowedOrigins.indexOf(origin) > -1){ 54 | res.setHeader('Access-Control-Allow-Origin', origin); 55 | } 56 | 57 | res.header("Access-Control-Allow-Credentials", "true"); 58 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 59 | res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH"); 60 | next(); 61 | }); 62 | 63 | var passport = require('./auth'); 64 | 65 | app.use(passport.initialize()); 66 | app.use(passport.session()); 67 | 68 | 69 | // routing 70 | var routes = require('./routes/index'); 71 | var estimations = require('./routes/estimations'); 72 | var projects = require('./routes/projects'); 73 | var users = require('./routes/users'); 74 | var estimationModels = require('./routes/estimationModels'); 75 | 76 | app.use('/', routes); 77 | app.use('/estimations', estimations); 78 | app.use('/projects', projects); 79 | app.use('/users', users); 80 | app.use('/estimationModels', estimationModels); 81 | 82 | 83 | 84 | 85 | 86 | module.exports = app; 87 | -------------------------------------------------------------------------------- /auth/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 08-Dec-16. 3 | */ 4 | var passport = require('passport'); 5 | // var yandexStrategy = require('passport-yandex').Strategy; 6 | var LocalStrategy = require('passport-local').Strategy; 7 | var User = require('./../Model/User').User; 8 | 9 | passport.serializeUser(function (user, done) { 10 | done(null, user.login); 11 | }); 12 | 13 | passport.deserializeUser(function (login, done) { 14 | User.findOne({login: login}, function (err, user) { 15 | err ? 16 | done(err, null) : 17 | done(null, user); 18 | }); 19 | }); 20 | 21 | passport.use(new LocalStrategy({ 22 | usernameField: 'login', 23 | passwordField: 'password', 24 | passReqToCallback: true, 25 | session: true 26 | }, 27 | function (req, login, password, done) { 28 | 29 | User.findOne({login: login}).select('+hashedPassword +salt').populate('_role').exec(function (err, user) { 30 | if (err) { 31 | done(err); 32 | } else if (!user) { 33 | done(null, false, {message: 'Incorrect username.'}); 34 | } else if (!user.checkPassword(password)) { 35 | done(null, false, {message: 'Incorrect password.'}); 36 | } else { 37 | done(null, user); 38 | } 39 | }) 40 | } 41 | )); 42 | 43 | module.exports = passport; 44 | 45 | module.exports.mustAuthenticated = function (req, res, next) { 46 | req.isAuthenticated() 47 | ? next() 48 | : res.status(401).json({auth: false}); 49 | }; -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('estimator:server'); 9 | var http = require('http'); 10 | 11 | var config = require('../config'); 12 | 13 | /** 14 | * Get port from environment and store in Express. 15 | */ 16 | 17 | var port = normalizePort(process.env.PORT || config.get('port')); 18 | app.set('port', port); 19 | 20 | /** 21 | * Create HTTP server. 22 | */ 23 | 24 | var server = http.createServer(app); 25 | 26 | /** 27 | * Listen on provided port, on all network interfaces. 28 | */ 29 | 30 | server.listen(port); 31 | server.on('error', onError); 32 | server.on('listening', onListening); 33 | 34 | /** 35 | * Normalize a port into a number, string, or false. 36 | */ 37 | 38 | function normalizePort(val) { 39 | var port = parseInt(val, 10); 40 | 41 | if (isNaN(port)) { 42 | // named pipe 43 | return val; 44 | } 45 | 46 | if (port >= 0) { 47 | // port number 48 | return port; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | /** 55 | * Event listener for HTTP server "error" event. 56 | */ 57 | 58 | function onError(error) { 59 | if (error.syscall !== 'listen') { 60 | throw error; 61 | } 62 | 63 | var bind = typeof port === 'string' 64 | ? 'Pipe ' + port 65 | : 'Port ' + port; 66 | 67 | // handle specific listen errors with friendly messages 68 | switch (error.code) { 69 | case 'EACCES': 70 | console.error(bind + ' requires elevated privileges'); 71 | process.exit(1); 72 | break; 73 | case 'EADDRINUSE': 74 | console.error(bind + ' is already in use'); 75 | process.exit(1); 76 | break; 77 | default: 78 | throw error; 79 | } 80 | } 81 | 82 | /** 83 | * Event listener for HTTP server "listening" event. 84 | */ 85 | 86 | function onListening() { 87 | var addr = server.address(); 88 | var bind = typeof addr === 'string' 89 | ? 'pipe ' + addr 90 | : 'port ' + addr.port; 91 | debug('Listening on ' + bind); 92 | } 93 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "mongoose": { 4 | "uri": "mongodb://127.0.0.1/estimator", 5 | "options": { 6 | "server": { 7 | "socketOptions": { 8 | "keepAlive": 1 9 | } 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 09-Nov-16. 3 | */ 4 | var nconf = require('nconf'); 5 | var path = require('path'); 6 | 7 | nconf.argv() 8 | .env() 9 | .file({file: path.join(__dirname, 'config.json')}); 10 | 11 | module.exports = nconf; -------------------------------------------------------------------------------- /db/StatusStorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 27-Dec-16. 3 | */ 4 | 5 | var colors = { 6 | default: 'blue-grey', 7 | primary: 'blue', 8 | warning: 'amber', 9 | info: 'green', 10 | danger: 'red', 11 | success: 'lime' 12 | }; 13 | 14 | var statuses = [ 15 | { 16 | order: 1, 17 | name: 'New', 18 | value: 'NEW', 19 | style: colors.default 20 | }, 21 | { 22 | order: 2, 23 | name: 'InProgress', 24 | value: 'IN_PROGRESS', 25 | style: colors.primary 26 | }, 27 | { 28 | order: 3, 29 | name: 'Questions', 30 | value: 'QUESTIONS', 31 | style: colors.warning 32 | }, 33 | { 34 | order: 4, 35 | name: 'Done', 36 | value: 'DONE', 37 | style: colors.info 38 | }, 39 | { 40 | order: 5, 41 | name: 'Sent', 42 | value: 'SENT', 43 | style: colors.danger 44 | }, 45 | { 46 | order: 6, 47 | name: 'Approved', 48 | value: 'APPROVED', 49 | style: colors.success 50 | }, 51 | { 52 | order: 7, 53 | name: 'InDevelopment', 54 | value: 'IN_DEVELOPMENT', 55 | style: colors.warning 56 | }, 57 | { 58 | order: 8, 59 | name: 'Closed', 60 | value: 'CLOSED', 61 | style: colors.success 62 | } 63 | ]; 64 | 65 | 66 | module.exports.statuses = statuses; -------------------------------------------------------------------------------- /db/createDb.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('../lib/mongoose'); 2 | var Estimation = require('../Model/Estimation'); 3 | var Project = require('../Model/Project'); 4 | var User = require('../Model/User'); 5 | var Role = require('../Model/Role'); 6 | 7 | mongoose.connection.on('open', function () { 8 | 9 | var db = mongoose.connection.db; 10 | db.dropDatabase(function (err) { 11 | if(err) throw err; 12 | }) 13 | 14 | }); -------------------------------------------------------------------------------- /error/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 08-Dec-16. 3 | */ 4 | var path = require('path'); 5 | var util = require('util'); 6 | var http = require('http'); 7 | 8 | // ошибки для выдачи посетителю 9 | function HttpError(status, message) { 10 | Error.apply(this, arguments); 11 | Error.captureStackTrace(this, HttpError); 12 | 13 | this.status = status; 14 | this.message = message || http.STATUS_CODES[status] || "Error"; 15 | } 16 | 17 | util.inherits(HttpError, Error); 18 | 19 | HttpError.prototype.name = 'HttpError'; 20 | 21 | exports.HttpError = HttpError; -------------------------------------------------------------------------------- /lib/mongoose.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 09-Nov-16. 3 | */ 4 | var mongoose = require('mongoose'); 5 | var config = require('../config'); 6 | 7 | mongoose.Promise = global.Promise; 8 | mongoose.connect(config.get('mongoose:uri'), config.get('mongoose:server')); 9 | 10 | module.exports = mongoose; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "estimator", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "async": "^2.1.4", 10 | "body-parser": "~1.13.2", 11 | "connect-ensure-login": "^0.1.1", 12 | "connect-mongodb-session": "^1.3.0", 13 | "cookie-parser": "^1.3.5", 14 | "debug": "~2.2.0", 15 | "express": "~4.13.1", 16 | "express-fileupload": "0.0.5", 17 | "express-session": "^1.14.2", 18 | "mongodb": "^2.2.11", 19 | "mongoose": "^4.6.6", 20 | "morgan": "~1.6.1", 21 | "nconf": "^0.8.4", 22 | "passport": "^0.3.2", 23 | "passport-local": "^1.0.0", 24 | "passport-yandex": "0.0.3", 25 | "serve-favicon": "~2.3.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiraffe/estimator/cd23806180dc89e9d97b1fc8e8d14efaac48ca0c/public/favicon.ico -------------------------------------------------------------------------------- /public/images/blank_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiraffe/estimator/cd23806180dc89e9d97b1fc8e8d14efaac48ca0c/public/images/blank_account.png -------------------------------------------------------------------------------- /routes/estimationModels.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 29-Dec-16. 3 | */ 4 | var express = require('express'); 5 | var router = express.Router(); 6 | var EstimationModel = require('../Model/EstimationModel').EstimationModel; 7 | var preparedModel = require('../Model/EstimationModel').preparedModel; 8 | var async = require('async'); 9 | 10 | var fields = []; 11 | (function () { 12 | EstimationModel.find({}, function (err, flds) { 13 | fields = flds; 14 | }); 15 | })(); 16 | 17 | router.get('/', function (req, res, next) { 18 | res.json(fields); 19 | }); 20 | 21 | router.get('/reinit', function (req, res, next) { 22 | async.series([ 23 | function (cb) { 24 | EstimationModel.remove({}, cb) 25 | }, 26 | function (cb) { 27 | 28 | async.mapSeries( 29 | preparedModel, 30 | (el, next) => new EstimationModel(el).save((err) => {err && console.log(err); next();}), 31 | (err) => cb() 32 | ) 33 | } 34 | ], 35 | function (err, results) { 36 | res.json({success: true}); 37 | }); 38 | 39 | }); 40 | 41 | module.exports = router; -------------------------------------------------------------------------------- /routes/estimations.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var Estimation = require('../Model/Estimation'); 4 | var statusStorage = require('../db/StatusStorage') 5 | 6 | router.get('/', function(req, res, next) { 7 | 8 | Estimation.find({}, function (err, estimations) { 9 | 10 | if(err) { 11 | res.status(500).send("Couldn't get estimations! \r\n" + err); 12 | } else { 13 | res.json(estimations); 14 | } 15 | }); 16 | }); 17 | 18 | router.get('/statuses', function (req, res, next) { 19 | res.json(statusStorage.statuses); 20 | }); 21 | 22 | router.get('/:key', function (req, res, next) { 23 | 24 | Estimation.findOne({key: req.params.key}, function (err, est) { 25 | 26 | if(err) { 27 | res.status(500).send("Couldn't get estimations! \r\n" + err); 28 | } else { 29 | res.json(est); 30 | } 31 | 32 | }); 33 | }); 34 | 35 | router.post("/", function (req, res, next) { 36 | 37 | var estimation = new Estimation(req.body); 38 | 39 | if(estimation.id) { 40 | updateMeta(req, res, next, estimation); 41 | } else { 42 | updateEstimation(req, res, next, estimation); 43 | } 44 | }); 45 | 46 | function updateMeta(req, res, next, estimation) { 47 | Estimation.findOneAndUpdate({_id: estimation.id}, estimation, {upsert: true}, function (err) { 48 | if (err) { 49 | res.send(err); 50 | } else { 51 | res.json({success: true}); 52 | } 53 | }); 54 | } 55 | 56 | function updateEstimation(req, res, next, estimation) { 57 | 58 | Estimation.findOneAndUpdate({key: estimation.key}, estimation, {upsert: true}, function (err) { 59 | if (err) { 60 | res.send(err); 61 | } else { 62 | res.json({success: true}); 63 | } 64 | }); 65 | } 66 | 67 | router.delete('/:key', function (req, res, next) { 68 | 69 | Estimation.remove({key: req.params.key}, function (err) { 70 | if(err) { 71 | res.status(500).send("Couldn't delete estimation " + req.params.key + "! \r\n" + err); 72 | } else { 73 | res.status(200).send(); 74 | } 75 | }); 76 | 77 | }); 78 | 79 | module.exports = router; 80 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /routes/projects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 13-Nov-16. 3 | */ 4 | var express = require('express'); 5 | var router = express.Router(); 6 | var Estimation = require('../Model/Estimation'); 7 | var Project = require('../Model/Project'); 8 | 9 | router.get("/", function (req, res, next) { 10 | 11 | Project.find({}, function (err, projects) { 12 | 13 | if(err) { 14 | res.status(500).send("Couldn't get projects! \r\n" + err); 15 | } else { 16 | res.json(projects); 17 | } 18 | }); 19 | }); 20 | 21 | router.get("/:key", function (req, res, next) { 22 | 23 | Project.find({key: req.params.key}, function (err, projects) { 24 | 25 | if(err) { 26 | res.status(500).send("Couldn't get projects! \r\n" + err); 27 | } else { 28 | res.json(projects); 29 | } 30 | }); 31 | }); 32 | 33 | router.get("/estimations/:key", function (req, res, next) { 34 | 35 | Estimation.find({projectKey: req.params.key}, function (err, estimations) { 36 | 37 | if(err) { 38 | res.status(500).send("Couldn't get estimations! \r\n" + err); 39 | } else { 40 | res.json(estimations); 41 | } 42 | }); 43 | 44 | }); 45 | 46 | router.post("/", function (req, res, next) { 47 | var project = new Project(req.body); 48 | 49 | Project.findOneAndUpdate({key: project.key}, project, {upsert: true}, function (err) { 50 | if (err) { 51 | res.send(err); 52 | } else { 53 | res.json({success: true}); 54 | } 55 | }); 56 | }); 57 | 58 | router.delete("/:key", function (req, res, next) { 59 | Project.remove({key: req.params.key}, function (err) { 60 | if (err) { 61 | res.status(500).send("Couldn't delete project " + req.params.key + "! \r\n" + err); 62 | } else { 63 | res.status(200).json({success: true}); 64 | } 65 | }) 66 | }); 67 | 68 | router.get('/:projectKey/estimation/:estimationKey', function (req, res, next) { 69 | Estimation.find({key: req.params.estimationKey, projectKey: req.params.projectKey}, function (err, estimation) { 70 | 71 | if(err) { 72 | res.status(500).send("Couldn't get estimation! \r\n" + err); 73 | } else { 74 | res.json(estimation); 75 | } 76 | }); 77 | }); 78 | 79 | router.post('/:projectKey/estimation/:estimationKey', function (req, res, next) { 80 | var estimation = new Estimation(req.body); 81 | 82 | if(estimation.id) { 83 | updateMeta(req, res, next, estimation); 84 | } else { 85 | updateEstimation(req, res, next, estimation); 86 | } 87 | }); 88 | 89 | function updateMeta(req, res, next, estimation) { 90 | Estimation.findOneAndUpdate({_id: estimation.id}, estimation, {upsert: true}, function (err) { 91 | if (err) { 92 | res.send(err); 93 | } else { 94 | res.json({success: true}); 95 | } 96 | }); 97 | } 98 | 99 | function updateEstimation(req, res, next, estimation) { 100 | 101 | Estimation.findOneAndUpdate({key: estimation.key}, estimation, {upsert: true}, function (err) { 102 | if (err) { 103 | res.send(err); 104 | } else { 105 | res.json({success: true}); 106 | } 107 | }); 108 | } 109 | 110 | module.exports = router; -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 08-Dec-16. 3 | */ 4 | var express = require('express'); 5 | var router = express.Router(); 6 | var User = require('../Model/User').User; 7 | var Role = require('../Model/Role'); 8 | var HttpError = require('../error').HttpError; 9 | var passport = require('../auth'); 10 | var path = require('path'); 11 | var fs = require('fs'); 12 | 13 | var userRole; 14 | var adminRole; 15 | 16 | (function () { 17 | Role.findOne({name:'admin'}, function (err, role) { 18 | adminRole = role; 19 | }); 20 | 21 | Role.findOne({name:'user'}, function (err, role) { 22 | userRole = role; 23 | }); 24 | })(); 25 | 26 | router.post('/login', function (req, res, next) { 27 | 28 | passport.authenticate('local', function (err, user, info) { 29 | if (err) { 30 | return next(err); 31 | } 32 | if (!user) { 33 | return res.status(500).json({success: false}); 34 | } 35 | req.logIn(user, function (err) { 36 | if (err) { 37 | return next(err); 38 | } 39 | user.hashedPassword = undefined; 40 | user.salt = undefined; 41 | user.role = 'admin'; 42 | return res.json(user); 43 | }); 44 | })(req, res, next); 45 | }); 46 | 47 | router.get('/password/reset', function (req, res, next) { 48 | User.findOne({email: req.query.email}, function (err, user) { 49 | if (err) { 50 | res.status(500).json({success: false, error: true}); 51 | } else if (!user) { 52 | res.status(500).json({success: false, notFound: true}); 53 | } else { 54 | var newPass = ""; 55 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 56 | 57 | for (var i = 0; i < 7; i++) 58 | newPass += possible.charAt(Math.floor(Math.random() * possible.length)); 59 | 60 | user.password = newPass; 61 | user.save(function (err) { 62 | if (err) { 63 | res.status(500).json({success: false, error: err}); 64 | } else { 65 | res.json({newPass: newPass}); 66 | } 67 | }); 68 | } 69 | 70 | }); 71 | }); 72 | 73 | router.post('/register', function (req, res, next) { 74 | var user = new User({ 75 | login: req.body.login, 76 | password: req.body.password, 77 | name: req.body.name, 78 | email: req.body.email, 79 | _role: userRole._id 80 | }); 81 | 82 | saveUserAndResponse(user, res, function (err, user) { 83 | var fp = path.join(__dirname, '../', 'public/users/',user.login,'/'); 84 | fs.mkdir(fp); 85 | 86 | res.json({success: true}); 87 | }); 88 | }); 89 | 90 | function saveUserAndResponse(user, res, next) { 91 | user.save(function (err) { 92 | return err 93 | ? 94 | err.errors ? 95 | res.status(500).json({success: false, errors: err.errors}) 96 | : 97 | res.status(500).json({success: false, errors: err.toJSON()}) 98 | : 99 | next ? 100 | next(err, user) : 101 | res.json({success: true}); 102 | }); 103 | } 104 | 105 | router.post('/profile/update', passport.mustAuthenticated, function (req, res, next) { 106 | 107 | User.findOne({login: req.session.passport.user}).populate('_role').exec(function (err, user) { 108 | if(err) res.send(err); 109 | 110 | user.name = req.body.name; 111 | if(req.body.password) { 112 | user.password = req.body.password; 113 | } 114 | user.language = req.body.language; 115 | 116 | saveUserAndResponse(user, res); 117 | }); 118 | }); 119 | 120 | router.get('/logout', function (req, res, next) { 121 | req.logout(); 122 | res.json({success: true}); 123 | }); 124 | 125 | router.get('/profile', passport.mustAuthenticated, function (req, res, next) { 126 | User.findOne({login: req.session.passport.user}).populate('_role').exec(function (err, user) { 127 | if(!user.avatarName) user.avatarName = "../../images/blank_account.png"; 128 | 129 | res.json(user); 130 | }); 131 | }); 132 | 133 | router.post('/changeAvatar', passport.mustAuthenticated, function (req, res, next) { 134 | 135 | User.findOne({login: req.session.passport.user}, function (err, user) { 136 | 137 | if (!req.files) { 138 | res.json({success: false}); 139 | return; 140 | } 141 | 142 | var file = req.files.file; 143 | var fp = path.join(__dirname, '../', 'public/users/',user.login,'/'); 144 | file.mv(fp + file.name, function (err) { 145 | if (err) { 146 | res.status(500).send(err); 147 | } 148 | else { 149 | user.avatarName = file.name; 150 | user.save(); 151 | res.json({success: true}); 152 | } 153 | }); 154 | }); 155 | }); 156 | 157 | module.exports = router; -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | host: '127.0.0.1', 3 | port: 2424, 4 | username: 'root', 5 | password: 'root' 6 | }; 7 | 8 | var OrientDB = require('orientjs'); 9 | var server = OrientDB(config); 10 | 11 | var db = server.use('VehicleHistoryGraph'); 12 | 13 | db.class.get('OUser').then(function(OUser) { 14 | OUser.list().then(function(records){ 15 | console.log('Found: ' + records.length); 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /views/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "libs" 3 | } 4 | -------------------------------------------------------------------------------- /views/app.js: -------------------------------------------------------------------------------- 1 | var host = window.location.host; 2 | var urlPrefix; 3 | if(host.indexOf('86.57.161.116') !== -1) { 4 | urlPrefix = 'http://86.57.161.116:3000/'; 5 | } else if(host.indexOf('192.168.1.60') !== -1 || host.indexOf('estimator') !== -1) { 6 | urlPrefix = 'http://192.168.1.60/'; 7 | } else { 8 | urlPrefix = 'http://127.0.0.1:3000/'; 9 | } 10 | 11 | var app = angular.module('estimator', [ 12 | 'ui.router', 13 | 'ngAnimate', 14 | 'ngAria', 15 | 'growl', 16 | 'ngCookies', 17 | 'ui.bootstrap', 18 | 'ui.bootstrap.contextMenu', 19 | 'ngMaterial', 20 | 'md.data.table', 21 | 'uiRouterStyles', 22 | 'ngFileUpload', 23 | 'ngResource' 24 | ]); 25 | 26 | 27 | app.config([ 28 | 29 | '$httpProvider', 30 | '$animateProvider', 31 | '$compileProvider', 32 | 33 | function($httpProvider, $animateProvider, $compileProvider) { 34 | 35 | $httpProvider.defaults.withCredentials = true; 36 | 37 | var requestInterceptor = function($q, StorageService){ 38 | return { 39 | 'request': function (config) { 40 | 41 | 42 | if(config.url.indexOf(".") === -1) { 43 | config.url = urlPrefix + config.url; 44 | } 45 | return config || $q.when(config); 46 | } 47 | } 48 | }; 49 | 50 | $httpProvider.interceptors.push(requestInterceptor); 51 | } 52 | ]); 53 | 54 | 55 | app.constant('AUTH_EVENTS', { 56 | loginSuccess: 'auth-login-success', 57 | loginFailed: 'auth-login-failed', 58 | logoutSuccess: 'auth-logout-success', 59 | sessionTimeout: 'auth-session-timeout', 60 | notAuthenticated: 'auth-not-authenticated', 61 | notAuthorized: 'auth-not-authorized', 62 | profileLoaded: 'user-profile-loaded', 63 | profileChanged: 'user-profile-changed', 64 | }); 65 | 66 | app.constant('USER_ROLES', { 67 | all: '*', 68 | admin: 'admin', 69 | user: 'user' 70 | }) 71 | 72 | app.run(function($rootScope, $state, StatusService, AUTH_EVENTS, AuthService) { 73 | $rootScope.$state = $state; 74 | 75 | AuthService.loadUser(); 76 | 77 | $rootScope.$on('$locationChangeStart', function (e, newURL, oldURL) { 78 | 79 | // if user reload page 80 | if(oldURL === newURL) { 81 | 82 | // for some reason location like 'project/MEDF/estimation/MEDF-64323' 83 | // detects as invalid by $location-service 84 | // so try to detect this and use appropriate state 85 | var urlKeys = newURL.split('/'); 86 | var sharpPos = urlKeys.indexOf('#'); 87 | urlKeys = urlKeys.splice(sharpPos + 1, urlKeys.length - sharpPos); 88 | if(urlKeys.length === 4 && urlKeys[0] === 'project' && urlKeys[2] === 'estimation') { 89 | e.preventDefault(); 90 | $state.go('projectEstimation', {projectKey: urlKeys[1], key: urlKeys[3]}); 91 | } 92 | } 93 | }); 94 | 95 | $rootScope.$on('stateChangeStart', function (event, next) { 96 | var authorizedRoles = next.data.authorizedRoles; 97 | if ( ! AuthService.isAuthorized(authorizedRoles)) { 98 | event.preventDefault(); 99 | if (AuthService.isAuthenticated()) { 100 | // user is not allowed 101 | $state.go('projects'); 102 | $rootScope.$broadcast(AUTH_EVENTS.notAuthorized); 103 | } else { 104 | // user is not logged in 105 | $state.go('login'); 106 | $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); 107 | } 108 | } 109 | }); 110 | 111 | $rootScope.$on('$stateChangeSuccess', function(e, next) { 112 | if (next.data) { 113 | document.title = next.data.title; 114 | } 115 | $rootScope.pageTitle = next.data.title; 116 | $rootScope.pageName = next.name; 117 | }); 118 | }); -------------------------------------------------------------------------------- /views/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "estimator", 3 | "authors": [ 4 | "Dzmitry Dubrovin " 5 | ], 6 | "description": "estimator", 7 | "main": "", 8 | "license": "MIT", 9 | "homepage": "", 10 | "private": true, 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "angular": "1.4.9", 20 | "angular-ui-router": "0.2.13", 21 | "bootstrap": "3.3.6", 22 | "angular-cookies": "^1.5.3", 23 | "angular-bootstrap": "0.14.3", 24 | "bower": "*", 25 | "install": "^1.0.4", 26 | "ng-contextmenu": "^0.7.2", 27 | "angular-bootstrap-contextmenu": "^0.9.9", 28 | "angular-material": "*", 29 | "angular-animate": "*", 30 | "angular-aria": "*", 31 | "angular-material-data-table": "^0.10.9", 32 | "angular-ui-router-styles": "^1.1.0", 33 | "ng-file-upload": "^12.2.13", 34 | "angular-resource": "^1.6.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /views/controllers/MainController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | angular.module('estimator') 4 | .controller('MainController', 5 | function ($scope, $http, $toast, $state, AuthService, USER_ROLES, AUTH_EVENTS, TranslationService) { 6 | 7 | $scope.params = angular.copy($state.params); 8 | $scope.user = {}; 9 | $scope.isAuthorized = AuthService.isAuthorized; 10 | $scope.hasAccessLevel = AuthService.hasAccessLevel; 11 | $scope.host = urlPrefix; 12 | 13 | $scope.logout = function () { 14 | AuthService.logout() 15 | .then(function () { 16 | $state.go('login'); 17 | $toast({message: $scope.translation.USER.MSGS.LOGOUT, theme: 'success'}) 18 | }); 19 | }; 20 | 21 | $scope.$on(AUTH_EVENTS.loginSuccess, function (event, data) { 22 | $scope.init(); 23 | }); 24 | 25 | $scope.$on(AUTH_EVENTS.profileLoaded, function (event, data) { 26 | $scope.init(); 27 | }); 28 | 29 | $scope.$on(AUTH_EVENTS.profileChanged, function (event, data) { 30 | $scope.init(); 31 | }); 32 | 33 | $scope.translate = function(){ 34 | TranslationService.getTranslation($scope); 35 | }; 36 | 37 | /* 38 | * Do not use in in ng-init! 39 | * Should be only executed after user profile 40 | * loaded(reloaded) in app.run function 41 | */ 42 | $scope.init = function () { 43 | $scope.user = AuthService.user; 44 | $scope.translate(); 45 | } 46 | }); -------------------------------------------------------------------------------- /views/controllers/estimation/EstimationAddController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 12-Nov-16. 3 | */ 4 | "use strict"; 5 | 6 | angular.module('estimator') 7 | .controller('EstimationAddController', [ 8 | 9 | '$scope', 10 | '$state', 11 | '$http', 12 | '$toast', 13 | 14 | function ($scope, $state, $http, $toast) { 15 | 16 | $scope.params = angular.copy($state.params); 17 | $scope.editMode = !!$scope.params.key; 18 | $scope.estimation = { 19 | key: undefined, 20 | projectKey: $scope.params.projectKey, 21 | status: { 22 | name: 'New', 23 | value: 'NEW', 24 | style: 'blue-grey' 25 | } 26 | }; 27 | 28 | $scope.init = function () { 29 | 30 | if ($scope.editMode) { 31 | initForEdit(); 32 | } else { 33 | initForCreate(); 34 | } 35 | }; 36 | 37 | function initForEdit() { 38 | $http.get('projects/' + $scope.params.projectKey + '/estimation/' + $scope.params.key) 39 | .then(function (res) { 40 | $scope.estimation = res.data[0]; 41 | }); 42 | } 43 | 44 | function initForCreate() { 45 | $http.get('projects/' + $scope.params.projectKey) 46 | .then(function (res) { 47 | let proj = res.data[0] 48 | $scope.estimation.estimationModel = proj.estimationModel; 49 | $scope.estimation.itsLink = proj.itsLink; 50 | }); 51 | }; 52 | 53 | 54 | $scope.$watch( 55 | 'estimation.key', 56 | function (newVal, oldVal) { 57 | if (!newVal) return; 58 | $scope.estimation.key = newVal.replace(/\/{0,}/g, ''); 59 | } 60 | ); 61 | 62 | $scope.add = function () { 63 | 64 | if (!$scope.addEstimationForm.$valid) { 65 | return; 66 | } 67 | 68 | var url = $scope.editMode ? 69 | 'projects/' + $scope.params.projectKey + '/estimation/' + $scope.params.key : 70 | 'estimations'; 71 | 72 | if (!$scope.editMode) { 73 | initDefaultSections(); 74 | } 75 | 76 | if ( ! $scope.estimation.estimationModel.estimationTimeNeeded && $scope.editMode) { 77 | $scope.estimation.analysis = undefined; 78 | } 79 | 80 | if ( 81 | $scope.estimation.estimationModel.estimationTimeNeeded && 82 | $scope.editMode && $scope.estimation.analysis.subSections.length === 0) { 83 | setAnalysisSection(); 84 | } 85 | 86 | $http({ 87 | url: 'estimations', 88 | method: 'POST', 89 | data: $scope.estimation 90 | }).success(function (res) { 91 | var message = $scope.editMode ? 92 | $scope.translation.ESTIMATIONS.MSGS.SAVED : 93 | $scope.translation.ESTIMATIONS.MSGS.ADDED; 94 | $toast({message: message, theme: 'success'}); 95 | $state.go('projectEstimations', {key: $scope.params.projectKey}); 96 | }); 97 | } 98 | 99 | function initDefaultSections() { 100 | $scope.estimation.sections = [{ 101 | name: undefined, 102 | subSections: [ 103 | { 104 | descr: undefined, 105 | estimation: [] 106 | } 107 | ] 108 | }]; 109 | 110 | if ($scope.estimation.estimationModel.estimationTimeNeeded) { 111 | setAnalysisSection(); 112 | } else { 113 | $scope.estimation.analysis = undefined; 114 | } 115 | } 116 | 117 | function setAnalysisSection() { 118 | $scope.estimation.analysis = { 119 | subSections: [ 120 | { 121 | descr : null, 122 | estimation: null 123 | } 124 | ] 125 | } 126 | } 127 | } 128 | 129 | ]); -------------------------------------------------------------------------------- /views/controllers/estimation/EstimationDetailController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 11-Nov-16. 3 | */ 4 | "use strict"; 5 | 6 | angular.module('estimator') 7 | .controller('EstimationDetailController', 8 | function ($scope, $state, $http, $stateParams, $toast, statuses, $q, $mdSidenav, translation) { 9 | 10 | 11 | $scope.params = angular.copy($state.params); 12 | $scope.estimation = {}; 13 | $scope.project = {}; 14 | $scope.esModel = {}; 15 | $scope.statuses = statuses; 16 | $scope.newComment = ''; 17 | 18 | // Mongo Express Angular Node 19 | 20 | $scope.statusBtn = { 21 | isOpen: false, 22 | hidden: false 23 | }; 24 | 25 | $scope.init = function () { 26 | getEstimation() 27 | .then(processSections) 28 | .then(prepareDevTotal) 29 | .then(prepareManagementSection) 30 | .then(prepareEstimationTotal); 31 | }; 32 | 33 | function getEstimation() { 34 | return $http({ 35 | url: 'projects/' + $scope.params.projectKey + '/estimation/' + $scope.params.key, 36 | method: 'GET', 37 | }).then(function (res) { 38 | $scope.estimation = res.data[0]; 39 | $scope.esModel = $scope.estimation.estimationModel; 40 | if (!$scope.estimation.sections || $scope.estimation.sections.length === 0) { 41 | $scope.estimation.sections = []; 42 | } 43 | }); 44 | }; 45 | 46 | function processSections() { 47 | $scope.estimation.sections.forEach(function (section, idx) { 48 | section.idx = idx; 49 | section.number = idx + 1; 50 | section.subSections.forEach(processSubsection, section); 51 | section.total = function () { 52 | return section.subSections.reduce((a, b) => a + b.total(), 0); 53 | } 54 | }); 55 | 56 | if ($scope.esModel.estimationTimeNeeded) { 57 | $scope.estimation.analysis.subSections.forEach((sub, idx) => sub.idx = idx); 58 | $scope.estimation.analysis.total = function () { 59 | var estimationTime = $scope.estimation.analysis.subSections.reduce((a, b) => a + b.estimation || 0, 0); 60 | $scope.estimation.estimationTime = estimationTime; 61 | return estimationTime; 62 | } 63 | } 64 | }; 65 | 66 | function prepareManagementSection() { 67 | $scope.estimation.devTotal(); 68 | var mngmntModel = $scope.esModel.mngmntModel; 69 | if (mngmntModel.length === 0) return; 70 | 71 | var mngmntSection = { 72 | name: 'Иное', 73 | total: function () { 74 | return $scope.estimation.mngmtSection.subSections.reduce((a, b) => a + b.estimation(), 0); 75 | }, 76 | subSections: [] 77 | }; 78 | 79 | mngmntModel.forEach(function (model) { 80 | var sub = { 81 | descr: model.name, 82 | estimation: function () { 83 | $scope.estimation.devTotal(); 84 | var preTotal = $scope.estimation.developmentTime * (model.percent / 100); 85 | 86 | //check if preTotal doesn't need to be rounded 87 | if(preTotal % 0.5 === 0) return preTotal; 88 | 89 | var frac = (preTotal % 1).toFixed(2); 90 | frac = frac * 100; 91 | 92 | if (frac < 25) frac = 0; 93 | else if (frac >= 25 && frac < 75) frac = 0.5; 94 | else frac = 1; 95 | // frac can be 0, 0.5 and 1. 96 | // So if preTotal < 1 and we add frac - too big value obtained 97 | if (preTotal < 1) 98 | return frac; 99 | 100 | // otherwise just add rounded fractional part(via frac) to floored preTotal 101 | var total = Math.floor(preTotal) + frac; 102 | return total || 0.5; 103 | } 104 | }; 105 | mngmntSection.subSections.push(sub); 106 | }); 107 | 108 | $scope.estimation.mngmtSection = mngmntSection; 109 | }; 110 | 111 | function prepareDevTotal() { 112 | $scope.estimation.devTotal = function () { 113 | var total = $scope.estimation.sections.reduce((a, b) => a + b.total(), 0); 114 | $scope.estimation.developmentTime = total; 115 | return total; 116 | } 117 | } 118 | 119 | function prepareEstimationTotal() { 120 | $scope.estimation.total = function () { 121 | var total = 0; 122 | total += $scope.estimation.mngmtSection.total(); 123 | total += $scope.estimation.sections.reduce((a, b) => a + b.total(), 0); 124 | 125 | if ($scope.esModel.estimationTimeNeeded) { 126 | total += $scope.estimation.analysis.total(); 127 | } 128 | 129 | $scope.estimation.totalTime = total; 130 | 131 | return total; 132 | } 133 | }; 134 | 135 | function processSubsection(sub, idx) { 136 | var section = this; 137 | sub.subNum = section.number + '.' + (idx + 1); 138 | sub.idx = idx; 139 | sub.total = function () { 140 | return sub.estimation.reduce((a, b) => a + b, 0); 141 | } 142 | }; 143 | 144 | function reinitTotals() { 145 | processSections(); 146 | prepareEstimationTotal(); 147 | }; 148 | 149 | $scope.changeStatus = function (status) { 150 | 151 | $scope.estimation.status = status; 152 | 153 | if(status.name === 'Approved') { 154 | $scope.estimation.approvedDate = Date.now(); 155 | } else if(status.name === 'InDevelopment') { 156 | $scope.estimation.workStartDate = Date.now(); 157 | } else if(status.name === 'Closed') { 158 | $scope.estimation.workEndDate = Date.now(); 159 | } 160 | 161 | $http({ 162 | url: 'estimations', 163 | method: 'POST', 164 | data: $scope.estimation 165 | }).success(function (res) { 166 | $toast({message: $scope.translation.ESTIMATIONS.MSGS.STATUS_CHANGED, theme: 'success'}); 167 | }); 168 | }; 169 | 170 | $scope.save = function (needExit) { 171 | $http({ 172 | url: 'projects/' + $scope.params.projectKey + '/estimation/' + $scope.params.key, 173 | method: 'POST', 174 | data: $scope.estimation 175 | }).success(function (res) { 176 | $toast({message: $scope.translation.ESTIMATIONS.MSGS.SAVED, theme: 'success'}); 177 | if (needExit) { 178 | $state.go('projectEstimations', {key: $scope.params.projectKey}); 179 | } 180 | }) 181 | }; 182 | 183 | $scope.toggleComments = function () { 184 | $mdSidenav("comments") 185 | .toggle(); 186 | }; 187 | 188 | $scope.closeComments = function () { 189 | $scope.toggleComments(); 190 | }; 191 | 192 | $scope.addComment = function () { 193 | if (!$scope.estimation.comments) $scope.estimation.comments = []; 194 | $scope.estimation.comments.push({text: $scope.newComment}); 195 | $scope.newComment = ''; 196 | }; 197 | $scope.sectionMenu = [ 198 | [translation.ESTIMATIONS.ACTIONS.ADD_SECTION, function (item) { 199 | var newItemIdx = item.$index + 1; 200 | var sectionNum = ($scope.estimation.sections && $scope.estimation.sections.length > 0) ? 201 | $scope.estimation.sections.length + 1 : 202 | 1; 203 | 204 | var newSection = { 205 | number: sectionNum, 206 | subSections: [ 207 | { 208 | subNum: sectionNum + 0.1, 209 | estimation: [] 210 | } 211 | ] 212 | }; 213 | 214 | $scope.estimation.sections.splice(newItemIdx, 0, newSection); 215 | 216 | reinitTotals(); 217 | }], 218 | null, 219 | [translation.ESTIMATIONS.ACTIONS.DELETE_SECTION, function (item) { 220 | 221 | // block delete last section 222 | if($scope.estimation.sections.length === 1) { 223 | $toast({message: $scope.translation.ESTIMATIONS.MSGS.LAST_SECTION, theme: 'warning'}); 224 | return; 225 | } 226 | 227 | $scope.estimation.sections.splice(item.section.idx, 1); 228 | reinitTotals(); 229 | }] 230 | ]; 231 | 232 | $scope.subSectionMenu = [ 233 | [translation.ESTIMATIONS.ACTIONS.ADD_SUBSECTION, function (item) { 234 | var sectionIdx = item.$parent.section.idx; 235 | var newItemIdx = item.$index + 1; 236 | 237 | var newSubSection = { 238 | estimation: [] 239 | }; 240 | 241 | $scope.estimation.sections[sectionIdx].subSections.splice(newItemIdx, 0, newSubSection); 242 | 243 | reinitTotals(); 244 | }], 245 | null, 246 | [translation.ESTIMATIONS.ACTIONS.DELETE_SUBSECTION, function (item) { 247 | 248 | // block delete last sub-section 249 | if($scope.estimation.sections.length === 1) { 250 | $toast({message: $scope.translation.ESTIMATIONS.MSGS.LAST_SUBSECTION, theme: 'warning'}); 251 | return; 252 | } 253 | 254 | var sectionIdx = item.$parent.section.idx; 255 | var subSectionIdx = item.sub.idx; 256 | 257 | $scope.estimation.sections[sectionIdx].subSections.splice(subSectionIdx, 1); 258 | reinitTotals(); 259 | }] 260 | ]; 261 | 262 | $scope.analysisSubSectionMenu = [ 263 | [translation.ESTIMATIONS.ACTIONS.ADD_SUBSECTION, function (item) { 264 | var newItemIdx = item.$index + 1; 265 | 266 | var newSubSection = { 267 | estimation: undefined 268 | }; 269 | 270 | $scope.estimation.analysis.subSections.splice(newItemIdx, 0, newSubSection); 271 | 272 | reinitTotals(); 273 | }], 274 | null, 275 | [translation.ESTIMATIONS.ACTIONS.DELETE_SUBSECTION, function (item) { 276 | var subSectionIdx = item.sub.idx; 277 | 278 | $scope.estimation.analysis.subSections.splice(subSectionIdx, 1); 279 | reinitTotals(); 280 | }] 281 | ]; 282 | 283 | $scope.print = function () { 284 | var DocumentContainer = document.getElementById('estimation'); 285 | var WindowObject = window.open('', 'PrintWindow', 'width=750,height=650,top=50,left=50,toolbars=no,scrollbars=yes,status=no,resizable=yes'); 286 | WindowObject.document.writeln(''); 287 | WindowObject.document.writeln('' + $scope.estimation.key + ''); 288 | WindowObject.document.writeln(''); 289 | WindowObject.document.writeln('') 290 | 291 | WindowObject.document.writeln(DocumentContainer.innerHTML); 292 | 293 | WindowObject.document.writeln(''); 294 | 295 | WindowObject.document.close(); 296 | WindowObject.focus(); 297 | }; 298 | } 299 | ) 300 | ; -------------------------------------------------------------------------------- /views/controllers/project/ProjectAddController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 14-Nov-16. 3 | */ 4 | "use strict"; 5 | 6 | angular.module('estimator') 7 | .controller('ProjectAddController', [ 8 | 9 | '$scope', 10 | '$state', 11 | '$http', 12 | '$toast', 13 | 14 | function ($scope, $state, $http, $toast) { 15 | 16 | var clearEstimationModel = { 17 | fields: [], 18 | estimationTimeNeeded: true, 19 | mngmntModel: [ 20 | { 21 | name: undefined, 22 | percent: undefined 23 | } 24 | ] 25 | }; 26 | $scope.params = angular.copy($state.params); 27 | $scope.project = { 28 | estimationModel: clearEstimationModel 29 | }; 30 | $scope.projectKey = $scope.params.key; 31 | 32 | $scope.upsert = function () { 33 | $http({ 34 | url: 'projects', 35 | method: 'POST', 36 | data: $scope.project 37 | }).success(function (res) { 38 | $toast({message: $scope.translation.PROJECTS.MSGS.SAVED, theme:'success'}); 39 | $state.go('projects'); 40 | }); 41 | }; 42 | 43 | $scope.initEditProject = function () { 44 | if ( ! $scope.projectKey) return; 45 | $http.get('projects/' + $scope.projectKey) 46 | .success(function (res) { 47 | $scope.project = res[0]; 48 | $scope.estimationModel = $scope.project.estimationModel || clearEstimationModel; 49 | }); 50 | }; 51 | 52 | $scope.addMngmntField = function() { 53 | $scope.project.estimationModel.mngmntModel.push({ 54 | name: undefined, 55 | percent: undefined 56 | }); 57 | }; 58 | 59 | $scope.removeMngmntField = function(idx) { 60 | $scope.project.estimationModel.mngmntModel.splice(idx, 1); 61 | }; 62 | 63 | $scope.init = function () { 64 | if ($scope.projectKey !== '') { 65 | $scope.initEditProject(); 66 | } 67 | } 68 | } 69 | 70 | ]); -------------------------------------------------------------------------------- /views/controllers/project/ProjectEstimationsController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 14-Nov-16. 3 | */ 4 | /** 5 | * Created by dzmitry_dubrovin on 11-Nov-16. 6 | */ 7 | "use strict"; 8 | 9 | angular.module('estimator') 10 | .controller('ProjectEstimationsController', [ 11 | 12 | '$scope', 13 | '$state', 14 | '$http', 15 | '$toast', 16 | 'statuses', 17 | 18 | function ($scope, $state, $http, $toast, statuses) { 19 | 20 | $scope.params = angular.copy($state.params); 21 | $scope.projectKey = $state.params.key; 22 | $scope.estimations = []; 23 | $scope.filtrations = [{ 24 | name: 'All', 25 | value: 'ALL' 26 | }]; 27 | $scope.filter = $scope.filtrations[0]; 28 | $scope.statuses = statuses; 29 | $scope.hideDone = true; 30 | $scope.selectedFields = []; 31 | $scope.estFields = []; 32 | $scope.primary = 'purple'; 33 | var baseFieldNames = ['summary', 'status']; 34 | 35 | function init() { 36 | getEstimations(); 37 | $http.get('estimationModels') 38 | .then(function (res) { 39 | $scope.estFields = res.data; 40 | $scope.estFields.forEach((el) => { 41 | if (baseFieldNames.indexOf(el.fieldName) !== -1) { 42 | $scope.selectedFields.push(el); 43 | } 44 | }); 45 | }) 46 | } 47 | 48 | init(); 49 | 50 | var needed = false; 51 | $scope.statusFilter = function (est) { 52 | 53 | needed = false; 54 | 55 | if ($scope.hideDone && est.status.name.toLowerCase() === 'closed') { 56 | return false; 57 | } 58 | 59 | if ($scope.filter.name === 'All') return true; 60 | else if ($scope.filter.name === est.status.name) { 61 | return true; 62 | } else { 63 | return false; 64 | } 65 | 66 | return needed; 67 | } 68 | 69 | $scope.deleteEstimation = function (key) { 70 | $http({ 71 | url: 'estimations/' + key, 72 | method: 'DELETE', 73 | }).success(function (res) { 74 | $toast({message: $scope.translation.ESTIMATIONS.MSGS.DELETED, theme: 'success'}); 75 | getEstimations(); 76 | }); 77 | }; 78 | 79 | function getEstimations() { 80 | $http({ 81 | url: 'projects/estimations/' + $scope.params.key, 82 | method: 'GET', 83 | }).then(function (res) { 84 | $scope.estimations = res.data; 85 | prepareFiltrations(); 86 | }); 87 | } 88 | 89 | function prepareFiltrations() { 90 | $scope.estimations.forEach(function (est) { 91 | 92 | var found = false; 93 | for (var i = 0; i < $scope.filtrations.length; i++) { 94 | if ($scope.filtrations[i].name == est.status.name) { 95 | found = true; 96 | break; 97 | } 98 | } 99 | 100 | if (!found) $scope.filtrations.push(est.status); 101 | }); 102 | } 103 | 104 | $scope.$watch( 105 | 'filter', 106 | function (newVal, oldVal) { 107 | if (newVal.name === 'Closed') { 108 | $scope.hideDone = false; 109 | } else { 110 | $scope.hideDone = true; 111 | } 112 | } 113 | ); 114 | } 115 | ]); -------------------------------------------------------------------------------- /views/controllers/project/ProjectsController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 14-Nov-16. 3 | */ 4 | "use strict"; 5 | 6 | angular.module('estimator') 7 | .controller('ProjectsController', [ 8 | 9 | '$scope', 10 | '$http', 11 | '$state', 12 | 'StorageService', 13 | 'BroadcastService', 14 | '$toast', 15 | 16 | function($scope, $http, $state, StorageService, BroadcastService, $toast) { 17 | 18 | $scope.params = angular.copy($state.params); 19 | $scope.projects = []; 20 | 21 | var getProjects = function() { 22 | $http({ 23 | method: 'GET', 24 | url: 'projects' 25 | }) 26 | .success(function (res) { 27 | $scope.projects = res; 28 | }); 29 | }; 30 | 31 | $scope.delete = function(projectKey) { 32 | $http({ 33 | method: 'DELETE', 34 | url: 'projects/' + projectKey 35 | }).success(function (res) { 36 | if(res.success) { 37 | $toast({message: $scope.translation.PROJECTS.MSGS.DELETED, theme:'success'}); 38 | getProjects(); 39 | } 40 | }) 41 | }; 42 | 43 | function init() { 44 | getProjects(); 45 | } 46 | init(); 47 | } 48 | 49 | ]); -------------------------------------------------------------------------------- /views/controllers/user/LoginController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | angular.module('estimator') 4 | .controller('LoginController', 5 | 6 | function ($scope, $http, $toast, $state, $mdDialog, AuthService, $rootScope, AUTH_EVENTS) { 7 | 8 | $scope.user = {}; 9 | $scope.mode = 'login'; // Register, Forgot password 10 | $scope.modes = { 11 | "login": "Вход в систему", 12 | "register": "Регистрация", 13 | "forgot password": "Восстановление пароля" 14 | } 15 | 16 | $scope.changeMode = function (mode) { 17 | $scope.mode = mode; 18 | }; 19 | 20 | $scope.login = function () { 21 | AuthService.login($scope.user) 22 | .then(function (user) { 23 | $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); 24 | $toast({message: "Вы успешно вошли под ником: " + $scope.user.login, theme: 'success'}); 25 | $state.go("projects"); 26 | }) 27 | .catch(function () { 28 | $toast({message: "Пользователь не найден или пароль не верен", theme: 'warning'}); 29 | }); 30 | }; 31 | 32 | $scope.resetPassword = function () { 33 | $http.get('users/password/reset', { 34 | params: {email: $scope.user.email} 35 | }) 36 | .success(function (res) { 37 | $toast({message: "Пароль изменён. Новый пароль: " + res.newPass, delay: 10000}); 38 | $scope.mode = 'login'; 39 | }); 40 | }; 41 | 42 | $scope.register = function () { 43 | 44 | prepareNewUser(); 45 | 46 | $http({ 47 | url: 'users/register', 48 | method: 'POST', 49 | data: $scope.user 50 | }) 51 | .success(function (res) { 52 | $toast({message: "Registration successful!", theme: 'success'}); 53 | $scope.mode = 'login'; 54 | }) 55 | .error(function (res) { 56 | console.log('error:', res); 57 | if (res.errors.code === 11000) { 58 | $mdDialog.show( 59 | $mdDialog.alert() 60 | .clickOutsideToClose(true) 61 | .title('Ну как так..') 62 | .textContent('Пользователь с таким логином или почтой уже существует.') 63 | .ok('Всё ясно!') 64 | ); 65 | } 66 | }); 67 | }; 68 | 69 | function prepareNewUser() { 70 | $scope.user.language = { 71 | key: 'ru', 72 | value: 'Русский' 73 | } 74 | } 75 | } 76 | ); -------------------------------------------------------------------------------- /views/controllers/user/UserProfileController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 11-Dec-16. 3 | */ 4 | angular.module('estimator') 5 | .controller('UserProfileController', 6 | 7 | function ($scope, $rootScope, $http, $toast, $state, AuthService, AUTH_EVENTS) { 8 | 9 | $scope.user = AuthService.user; 10 | 11 | $scope.languages = [{ 12 | key: 'en', 13 | value: 'English' 14 | },{ 15 | key: 'ru', 16 | value: 'Русский' 17 | }]; 18 | 19 | $scope.save = function () { 20 | $http({ 21 | url: 'users/profile/update', 22 | method: 'POST', 23 | data: $scope.user 24 | }) 25 | .then(function (res) { 26 | AuthService.user = $scope.user; 27 | $rootScope.$broadcast(AUTH_EVENTS.profileChanged); 28 | $toast({message:$scope.translation.USER.MSGS.PROFILE_CHANGED, theme: 'success'}); 29 | }) 30 | }; 31 | 32 | $scope.changeAvatar = function() { 33 | var formData = new FormData(); 34 | formData.append('file', $scope.picFile); 35 | $http.post('users/changeAvatar', formData, { 36 | transformRequest: angular.identity, 37 | headers: {'Content-Type': undefined} 38 | }).then(function(){ 39 | $rootScope.$broadcast(AUTH_EVENTS.profileChanged); 40 | $scope.user.avatarName = $scope.picFile.name; 41 | $scope.picFile = undefined; 42 | },function(err){ 43 | console.err(err); 44 | }); 45 | }; 46 | 47 | $scope.init = function () { 48 | $scope.languages.forEach(function (lang, idx) { 49 | if(lang.key === $scope.user.language.key) { 50 | $scope.user.language = $scope.languages[idx]; 51 | } 52 | }) 53 | } 54 | }); -------------------------------------------------------------------------------- /views/directives/confirmPopup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('estimator') 4 | .directive('confirmPopup', function($mdDialog) { 5 | return { 6 | restrict: 'A', 7 | link: function($scope, $elem) { 8 | var action = 'click'; 9 | if ($scope.change) { 10 | action = 'change'; 11 | } 12 | $elem.on(action, function () { 13 | 14 | $mdDialog.show({ 15 | templateUrl: '/views/popups/confirmPopup.html', 16 | controller: 'ConfirmPopupCtrl', 17 | resolve: { 18 | params: function () { 19 | return { 20 | confirmMethod : $scope.confirmMethod, 21 | cancelMethod: $scope.cancelMethod, 22 | confirmTitle : $scope.confirmTitle, 23 | confirmText : $scope.confirmText, 24 | timeoutMethod: $scope.timeoutMethod, 25 | isDisabled: $scope.isDisabled, 26 | reasonDisabled: $scope.reasonDisabled 27 | }; 28 | }, 29 | buttons: function () { 30 | return { 31 | confirmButton: $scope.confirmButton, 32 | cancelButton: $scope.cancelButton 33 | }; 34 | } 35 | } 36 | }); 37 | }); 38 | }, 39 | scope: { 40 | confirmMethod: '&', 41 | cancelMethod: '&', 42 | timeoutMethod: '&', 43 | confirmTitle: '@', 44 | confirmText: '@', 45 | isDisabled: '@', 46 | reasonDisabled: '@', 47 | size: '@', 48 | confirmButton: '@', 49 | cancelButton: '@', 50 | change: '@' 51 | } 52 | }; 53 | }) 54 | .controller('ConfirmPopupCtrl', function ($scope, params, buttons, $mdDialog) { 55 | 56 | 57 | $scope.params = angular.copy(params); 58 | $scope.buttons = angular.copy(buttons); 59 | 60 | $scope.buttons.confirmButton = $scope.buttons.confirmButton || 'Да'; 61 | $scope.buttons.cancelButton = $scope.buttons.cancelButton || 'Нет'; 62 | 63 | $scope.params.confirmTitle = $scope.params.confirmTitle || 'Подтверждение'; 64 | $scope.params.confirmText = $scope.params.confirmText || 'Подтвердите действие'; 65 | $scope.params.isDisabled = $scope.params.isDisabled || false; 66 | $scope.params.reasonDisabled = $scope.params.reasonDisabled || $scope.params.confirmTitle; 67 | 68 | $scope.execute = function () { 69 | if($scope.params.confirmMethod) { 70 | $scope.params.confirmMethod(); 71 | } 72 | $mdDialog.hide(); 73 | }; 74 | 75 | $scope.cancel = function() { 76 | if($scope.params.cancelMethod) { 77 | $scope.params.cancelMethod(); 78 | } 79 | $mdDialog.cancel(); 80 | }; 81 | 82 | 83 | function init() { 84 | if($scope.params.timeoutMethod) $scope.params.timeoutMethod(); 85 | } 86 | 87 | init(); 88 | }); -------------------------------------------------------------------------------- /views/directives/contenteditable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 13-Nov-16. 3 | */ 4 | 5 | angular.module('estimator') 6 | .directive('contenteditable', function($uibModal) { 7 | 8 | return { 9 | restrict: 'A', 10 | require: 'ngModel', 11 | scope: { 12 | type: '@' 13 | }, 14 | link: function (scope, element, attrs, ngModel) { 15 | 16 | ngModel.$render = function () { 17 | element.html(ngModel.$viewValue); 18 | }; 19 | 20 | element.on('blur', function () { 21 | scope.$apply(read); 22 | }); 23 | 24 | function read() { 25 | var html = element.html(); 26 | 27 | var tmp = document.createElement("DIV"); 28 | tmp.innerHTML = html; 29 | var value = tmp.textContent || tmp.innerText || ""; 30 | 31 | if(attrs.type === 'number') { 32 | value = value.replace(',', '.'); 33 | ngModel.$setViewValue(+value); 34 | } else { 35 | ngModel.$setViewValue(value); 36 | } 37 | } 38 | } 39 | } 40 | 41 | }); -------------------------------------------------------------------------------- /views/directives/equality.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 11-Dec-16. 3 | */ 4 | 5 | app.directive('equality', function($q) { 6 | return { 7 | require: 'ngModel', 8 | link: function($scope, $elem, $attr, ctrl) { 9 | ctrl.$validators.equality = function(value) { 10 | return value === $attr["equality"]; 11 | }; 12 | } 13 | }; 14 | }); -------------------------------------------------------------------------------- /views/directives/growl.js: -------------------------------------------------------------------------------- 1 | var growl = angular.module('growl', [ ]) 2 | 3 | .service('$growl', ['$timeout', function ($timeout) { 4 | 5 | var growls = [ ]; 6 | var style = 'default'; 7 | var delayTime = 5000; 8 | 9 | var type; 10 | 11 | var icon = { 12 | info: 'glyphicon-info-sign', 13 | success: 'glyphicon-ok-sign', 14 | warning: 'glyphicon-exclamation-sign', 15 | danger: 'glyphicon-remove-sign' 16 | }; 17 | 18 | var configuredDelayTime; 19 | 20 | this.addMessage = function (newHeaders, newMessage, newType) { 21 | if(newType in icon === false){ 22 | newType = 'info'; 23 | } 24 | if(style === 'default') { 25 | type = 'alert-'+ newType; 26 | } else if(style = 'gray') { 27 | type = 'alert-gray ' + newType; 28 | } 29 | 30 | growls.push({header: newHeaders, message: newMessage, type: type, iconStyle: icon[newType]}); 31 | $timeout(function () { 32 | growls.splice(0, 1); 33 | }, delayTime); 34 | }; 35 | 36 | this.getGrowls = function() { 37 | return growls; 38 | }; 39 | 40 | this.setStyle = function(newStyle) { 41 | style = newStyle; 42 | }; 43 | 44 | this.setDelayTime = function(newDelayTime) { 45 | delayTime = newDelayTime; 46 | }; 47 | 48 | this.setConfiguredDelayTime = function(delayTime) { 49 | configuredDelayTime = delayTime; 50 | }; 51 | }]) 52 | 53 | .controller('growlCtrl', ['$growl', function($growl) { 54 | 55 | this.setStyle = function(newStyle) { 56 | $growl.setStyle(newStyle); 57 | }; 58 | 59 | this.setDelayTime = function(newDelayTime) { 60 | $growl.setDelayTime(newDelayTime); 61 | }; 62 | 63 | this.setConfiguredDelayTime = function(newDelayTime) { 64 | $growl.setConfiguredDelayTime(newDelayTime); 65 | }; 66 | 67 | this.getGrowls = function() { 68 | return $growl.getGrowls(); 69 | }; 70 | 71 | }]) 72 | 73 | .directive('growl', function() { 74 | return { 75 | restrict: 'E', 76 | template: '
' + 77 | '
' + 78 | '
' + 79 | '' + 80 | '
' + 81 | '
' + 82 | '
{{growl.header}}

{{growl.message}}

' + 83 | '
' + 84 | '
' + 85 | '
' + 86 | '
', 87 | controller: 'growlCtrl', 88 | link: function(scope, elem, attrs, ctrl) { 89 | ctrl.setStyle(attrs.style || 'default'); 90 | ctrl.setDelayTime(attrs.delayTime || 5000); 91 | ctrl.setConfiguredDelayTime(attrs.delayTime || 5000); 92 | scope.growls = ctrl.getGrowls(); 93 | } 94 | 95 | }; 96 | }); 97 | -------------------------------------------------------------------------------- /views/error.html: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Estimator 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /views/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | app.config(function ($urlRouterProvider, $stateProvider, $locationProvider) { 4 | 5 | $urlRouterProvider.otherwise('/login'); 6 | $urlRouterProvider.when('/', '/projects'); 7 | /*if(navigator.appVersion.indexOf("MSIE 8.") === -1 && navigator.appVersion.indexOf("MSIE 9.") === -1){ 8 | $locationProvider.html5Mode(true); 9 | }*/ 10 | 11 | $stateProvider.state('base', { 12 | templateUrl: 'views/templates/main.html', 13 | }) 14 | .state('index', { 15 | parent: 'base', 16 | url: '/', 17 | templateUrl: 'views/pages/index.html', 18 | data: { 19 | title: 'Оценки' 20 | } 21 | }) 22 | .state('addEstimation', { 23 | parent: 'base', 24 | url: '/estimation/add/:projectKey', 25 | controller: 'EstimationAddController', 26 | templateUrl: 'views/pages/estimation/estimationAdd.html', 27 | data: { 28 | title: 'Оценка' 29 | } 30 | }) 31 | .state('editEstimation', { 32 | parent: 'base', 33 | url: 'project/:projectKey/estimation/:key/edit', 34 | controller: 'EstimationAddController', 35 | templateUrl: 'views/pages/estimation/estimationAdd.html', 36 | data: { 37 | title: 'Оценка' 38 | } 39 | }) 40 | .state('estimation', { 41 | parent: 'base', 42 | url: '/estimation/:key', 43 | controller: 'EstimationDetailController', 44 | templateUrl: 'views/pages/estimation/estimation.html', 45 | data: { 46 | title: 'Оценка' 47 | }, 48 | resolve: { 49 | 'statuses': function (StatusService) { 50 | return StatusService.promise; 51 | } 52 | } 53 | }) 54 | .state('projectEstimation', { 55 | parent: 'base', 56 | url: 'project/:projectKey/estimation/:key', 57 | controller: 'EstimationDetailController', 58 | templateUrl: 'views/pages/estimation/estimation.html', 59 | data: { 60 | title: 'Оценка' 61 | }, 62 | resolve: { 63 | 'statuses': function (StatusService) { 64 | return StatusService.promise; 65 | }, 66 | 'translation': function (TranslationService) { 67 | return TranslationService.promise; 68 | } 69 | } 70 | }) 71 | .state('projects', { 72 | parent: 'base', 73 | url: '/projects', 74 | controller: 'ProjectsController', 75 | templateUrl: 'views/pages/project/projects.html', 76 | data: { 77 | title: 'Проекты' 78 | } 79 | }) 80 | .state('projectEstimations', { 81 | parent: 'base', 82 | url: '/project/:key/estimations', 83 | controller: 'ProjectEstimationsController', 84 | templateUrl: 'views/pages/estimation/estimations.html', 85 | data: { 86 | title: 'Оценки проекта' 87 | }, 88 | resolve: { 89 | 'statuses': function (StatusService) { 90 | return StatusService.promise; 91 | } 92 | } 93 | }) 94 | .state('addProject', { 95 | parent: 'base', 96 | url: '/project/add', 97 | controller: 'ProjectAddController', 98 | templateUrl: 'views/pages/project/projectAdd.html', 99 | data: { 100 | title: 'Создать проект' 101 | } 102 | }) 103 | .state('editProject', { 104 | parent: 'base', 105 | url: '/project/edit/:key', 106 | controller: 'ProjectAddController', 107 | templateUrl: 'views/pages/project/projectAdd.html', 108 | data: { 109 | title: 'Редактировать проект' 110 | } 111 | }) 112 | 113 | //User operations 114 | .state('login', { 115 | url: '/login', 116 | controller: 'LoginController', 117 | templateUrl: 'views/pages/user/login.html', 118 | data: { 119 | title: 'Войти' 120 | } 121 | }) 122 | .state('profile', { 123 | parent: 'base', 124 | url: '/user/profile', 125 | controller: 'UserProfileController', 126 | templateUrl: 'views/pages/user/profile.html', 127 | data: { 128 | title: 'Профиль пользователя' 129 | } 130 | }) 131 | } 132 | ); -------------------------------------------------------------------------------- /views/services/$toast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 03-Dec-16. 3 | */ 4 | angular.module('estimator') 5 | .service('$toast', [ 6 | 7 | '$mdToast', 8 | 9 | function ($mdToast) { 10 | 11 | /** 12 | * @param opts apply 13 | * @code theme - style for toast. From md-colors 14 | * @code delay - how long toast stays at page. Default 2 sec. 15 | * @code message - toast message 16 | */ 17 | var API = function (opts) { 18 | 19 | theme = (opts.theme || 'default') + '-toast-theme'; 20 | 21 | $mdToast.show({ 22 | templateUrl: 'views/popups/toast.html', 23 | hideDelay: opts.delay || 2000, 24 | position: 'top right', 25 | controller: 'ToastController', 26 | locals: {message: opts.message}, 27 | toastClass: theme 28 | }); 29 | }; 30 | 31 | return API; 32 | }]) 33 | 34 | .controller('ToastController', function ($scope, message, $mdToast) { 35 | 36 | $scope.message = message; 37 | 38 | $scope.closeToast = function () { 39 | $mdToast.hide(); 40 | }; 41 | }) -------------------------------------------------------------------------------- /views/services/AuthService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 13-Dec-16. 3 | */ 4 | angular.module('estimator') 5 | .factory('AuthService', 6 | function ($http, Session, $q, $state, $rootScope, AUTH_EVENTS, TranslationService) { 7 | 8 | var API = {}; 9 | 10 | API.user = undefined; 11 | 12 | API.login = function (user) { 13 | return $http({ 14 | url: 'users/login', 15 | method: 'POST', 16 | data: user 17 | }) 18 | .then(function (res) { 19 | res = res.data; 20 | API.user = res; 21 | initProfileImage(); 22 | Session.create(res._id, res.login, res._role); 23 | return res; 24 | }) 25 | }; 26 | 27 | API.isAuthenticated = function () { 28 | return !!Session.userId; 29 | }; 30 | 31 | API.hasAccessLevel = function (requiredLevel) { 32 | return API.user._role.accessLevel >= requiredLevel; 33 | }; 34 | 35 | API.isAuthorized = function (authorizedRoles) { 36 | if (!angular.isArray(authorizedRoles)) { 37 | authorizedRoles = [authorizedRoles]; 38 | } 39 | return (authService.isAuthenticated() && 40 | authorizedRoles.indexOf(Session.userRole) !== -1); 41 | }; 42 | 43 | API.logout = function () { 44 | return $http.get('users/logout') 45 | .then(function () { 46 | Session.destroy(); 47 | }) 48 | }; 49 | 50 | API.loadUser = function () { 51 | return $http.get('users/profile') 52 | .then(function (res) { 53 | API.user = res.data; 54 | TranslationService.currentLang = API.user.language.key; 55 | initProfileImage(); 56 | $rootScope.$broadcast(AUTH_EVENTS.profileLoaded, API.user); 57 | if($state.current.name === 'login') { 58 | $state.go('projects'); 59 | } 60 | }) 61 | .catch(function (err) { 62 | $state.go('login') 63 | }) 64 | }; 65 | 66 | var blankProfile = '../../images/blank_account.png'; 67 | function initProfileImage() { 68 | API.user.avatarName = API.user.avatarName ? API.user.avatarName : blankProfile; 69 | } 70 | 71 | $rootScope.$on(AUTH_EVENTS.profileChanged, function(evt) { 72 | API.loadUser(); 73 | }); 74 | 75 | return API; 76 | }); -------------------------------------------------------------------------------- /views/services/BroadcastService.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | angular.module('estimator') 4 | .service('BroadcastService', [ 5 | 6 | '$rootScope', 7 | 8 | function($rootScope){ 9 | 10 | var action = function (eventKey,action,timeout) { 11 | if(timeout){ 12 | setTimeout(function() {$rootScope.$broadcast(eventKey, action);}, 20); 13 | }else{ 14 | $rootScope.$broadcast(eventKey, action); 15 | } 16 | 17 | 18 | }; 19 | 20 | var onAction = function (eventKey,$scope, handler) { 21 | $scope.$on(eventKey, function (event, data) { 22 | handler(data); 23 | }); 24 | }; 25 | 26 | 27 | return { 28 | onAction: onAction, 29 | action:action, 30 | }; 31 | } 32 | ]); -------------------------------------------------------------------------------- /views/services/Session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 13-Dec-16. 3 | */ 4 | angular.module('estimator') 5 | .service('Session', 6 | function () { 7 | this.create = function (userId, userLogin, userRole) { 8 | this.userId = userId; 9 | this.userLogin = userLogin; 10 | this.userRole = userRole; 11 | }; 12 | this.destroy = function () { 13 | this.userId = undefined; 14 | this.userLogin = undefined; 15 | this.userRole = undefined; 16 | }; 17 | }); -------------------------------------------------------------------------------- /views/services/StatusService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 16-Nov-16. 3 | */ 4 | "use strict"; 5 | 6 | angular.module('estimator') 7 | .service('StatusService', function($http){ 8 | 9 | var statuses = []; 10 | 11 | var promise = $http({method:'GET', url:'estimations/statuses'}).then(function (res) { 12 | statuses = res.data; 13 | return res.data; 14 | }); 15 | 16 | return { 17 | statuses: statuses, 18 | promise: promise 19 | }; 20 | } 21 | ); -------------------------------------------------------------------------------- /views/services/StorageService.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | angular.module('estimator') 4 | .service('StorageService', [ 5 | 6 | '$cookieStore', 7 | 8 | function($cookieStore) { 9 | 10 | var storageType = { 11 | LOCAL_STORAGE: 'LOCAL_STORAGE', 12 | COOKIE: 'COOKIE' 13 | } 14 | 15 | var configs = { 16 | Authorization: { 17 | name: 'Authorization', 18 | storage: storageType.LOCAL_STORAGE 19 | }, 20 | CurrentUserId: { 21 | name: 'currentUserId', 22 | storage: storageType.LOCAL_STORAGE 23 | } 24 | }; 25 | 26 | var STORAGE_OPERATIONS = { 27 | LOCAL_STORAGE: { 28 | GET: function(configName) { 29 | return localStorage.getItem(configName); 30 | }, 31 | SET: function(configName, value) { 32 | localStorage.setItem(configName, value); 33 | }, 34 | DELETE: function(configName) { 35 | localStorage.removeItem(configName); 36 | } 37 | }, 38 | 39 | COOKIE : { 40 | GET: function(configName) { 41 | return $cookieStore.get(configName); 42 | }, 43 | SET: function(configName, value) { 44 | $cookieStore.put(configName, value); 45 | }, 46 | DELETE: function(configName) { 47 | $cookieStore.remove(configName); 48 | } 49 | } 50 | } 51 | 52 | var OPERATIONS = { 53 | GET: 'GET', 54 | SET: 'SET', 55 | DELETE: 'DELETE' 56 | }; 57 | 58 | var processOperation = function(operation, config, value) { 59 | var storageType = config.storage; 60 | var fn = STORAGE_OPERATIONS[storageType][operation]; 61 | return fn(config.name, value); 62 | }; 63 | 64 | var removeAllConfigs = function() { 65 | for(var configKey in configs) { 66 | processOperation(OPERATIONS.DELETE, configs[configKey]); 67 | } 68 | }; 69 | 70 | return { 71 | configs: configs, 72 | get: function(config) { 73 | return processOperation(OPERATIONS.GET, config); 74 | }, 75 | set: function(config, value) { 76 | return processOperation(OPERATIONS.SET, config, value); 77 | }, 78 | remove: function (config) { 79 | return processOperation(OPERATIONS.DELETE, config); 80 | }, 81 | deleteAll: function() { 82 | return removeAllConfigs(); 83 | } 84 | } 85 | 86 | } 87 | ]); -------------------------------------------------------------------------------- /views/services/TranslationService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dzmitry_dubrovin on 25-Dec-16. 3 | */ 4 | angular.module('estimator') 5 | .service('TranslationService', function ($resource, $http) { 6 | 7 | var API = {}; 8 | API.translation = {}; 9 | API.currentLang = 'ru'; 10 | 11 | var getLangPath = function (lang) { 12 | return 'translations/translation_' + API.currentLang + '.json'; 13 | }; 14 | 15 | API.getTranslation = function ($scope) { 16 | $resource(getLangPath()).get(function (data) { 17 | self.translation = data; 18 | $scope.translation = data; 19 | }); 20 | }; 21 | 22 | API.promise = $http({method:'GET', url:'translations/translation_' + API.currentLang + '.json'}).then(function (res) { 23 | API.translation = res.data; 24 | return res.data; 25 | }); 26 | 27 | return API; 28 | }); -------------------------------------------------------------------------------- /views/styles/estimationTable.css: -------------------------------------------------------------------------------- 1 | 2 | /* DivTable.com */ 3 | .divTable{ 4 | display: table; 5 | width: 100%; 6 | font-family: arial, sans-serif; 7 | font-size: 13px; 8 | } 9 | .divTableRow { 10 | display: table-row; 11 | width: inherit; 12 | } 13 | .divTableHeading { 14 | background-color: white; 15 | display: table-header-group; 16 | } 17 | .divTableCell, .divTableHead { 18 | border-right: 1px solid #DADADA; 19 | border-top: 1px solid #DADADA; 20 | display: table-cell; 21 | padding: 3px 10px; 22 | } 23 | .divTableFoot { 24 | background-color: white; 25 | display: table-footer-group; 26 | } 27 | .divTableBody { 28 | display: table-row-group; 29 | } 30 | .divTableRowGroup { 31 | 32 | } 33 | .divTableHead { 34 | background-color: #38761D; 35 | text-align: center; 36 | vertical-align: middle; 37 | font-weight: bold; 38 | color: white; 39 | } 40 | .summary { 41 | background-color: #0C343D; 42 | text-align: center; 43 | vertical-align: middle; 44 | font-weight: bold; 45 | color: white; 46 | height: 40px; 47 | } 48 | .section { 49 | background-color: #ff9900; 50 | font-weight: bold; 51 | color: white; 52 | } 53 | .divEstimation { 54 | background-color: #b45f06; 55 | font-weight: bold; 56 | color: white; 57 | } 58 | .sectionNumber { 59 | width: 58px; 60 | } 61 | .estVal { 62 | width: 50px; 63 | } 64 | .floatRight { 65 | text-align: right; 66 | } 67 | 68 | .comment { 69 | display: block; 70 | font-size: 14px; 71 | line-height: 19.6px; 72 | list-style-position: outside; 73 | list-style-type: none; 74 | padding-bottom: 5px; 75 | text-align: left; 76 | text-size-adjust: 100%; 77 | } 78 | 79 | @media print { 80 | .divTableHead,.summary{text-align:center;vertical-align:middle}.divEstimation,.divTableHead,.section,.summary{font-weight:700;color:#fff}.divTable{display:table;width:100%;font-family:arial,sans-serif;font-size:13px}.divTableRow{display:table-row;width:inherit}.divTableHeading{background-color:#EEE;display:table-header-group}.divTableCell,.divTableHead{border:1px solid #DADADA;display:table-cell;padding:3px 10px}.divTableFoot{background-color:#fff;display:table-footer-group}.divTableBody{display:table-row-group}.divTableHead{background-color:#38761D}.summary{background-color:#0C343D;height:40px}.section{background-color:#f90}.divEstimation{background-color:#b45f06}.sectionNumber{width:58px}.estVal{width:50px}.floatRight{text-align:right} 81 | } -------------------------------------------------------------------------------- /views/styles/growl.min.css: -------------------------------------------------------------------------------- 1 | .box{width:350px;position:fixed;top:15px;right:15px;z-index:3100} 2 | .icon{top:10px;left:10px;font-size:24px} 3 | .growl{box-shadow:0 0 10px #a4a4a4} 4 | .alert-gray{background-color:#e2e2e2;border-color:#b9b9b9;background-image:linear-gradient(to bottom,#fafafa 0,#d8d8d8 100%)} 5 | .danger{color:#df3a01}.info{color:#2e64fe}.warning{color:#ffbf00}.success{color:#088a29} -------------------------------------------------------------------------------- /views/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fafafa; 3 | } 4 | 5 | #estimation { 6 | padding: 20px 5%; 7 | } 8 | 9 | #estimationButtons { 10 | padding: 0px 5%; 11 | } 12 | 13 | .est-table { 14 | width: 100%; 15 | } 16 | 17 | .centeate-text { 18 | vertical-align: middle; 19 | text-align: center; 20 | } 21 | 22 | .middle-input-margin { 23 | margin-top: 6px; 24 | } 25 | 26 | .custom-chips md-chip .md-chip-remove-container { 27 | position: absolute; 28 | right: 4px; 29 | top: 4px; 30 | margin-right: 0; 31 | height: 24px; 32 | } 33 | 34 | .custom-chips md-chip .md-chip-remove-container button.estFieldBtn { 35 | position: relative; 36 | height: 24px; 37 | width: 24px; 38 | line-height: 30px; 39 | text-align: center; 40 | background: rgba(0, 0, 0, 0.3); 41 | border-radius: 50%; 42 | border: none; 43 | box-shadow: none; 44 | padding: 0; 45 | margin: 0; 46 | transition: background 0.15s linear; 47 | display: block; 48 | } 49 | 50 | .custom-chips md-chip .md-chip-remove-container button.estFieldBtn md-icon { 51 | position: absolute; 52 | top: 50%; 53 | left: 50%; 54 | transform: translate3d(-50%, -50%, 0) scale(0.7); 55 | color: white; 56 | fill: white; 57 | } 58 | 59 | .inputWithSwich { 60 | margin-top: 35px; 61 | margin-left: 30px; 62 | } 63 | 64 | .success-toast-theme .md-toast-content { 65 | background-color: #339933; 66 | font-weight: bold; 67 | } 68 | 69 | .error-toast-theme .md-toast-content { 70 | background-color: #800000; 71 | font-weight: bold; 72 | } 73 | 74 | .warning-toast-theme .md-toast-content { 75 | background-color: #FFCC00; 76 | color: black; 77 | font-weight: bold; 78 | } 79 | 80 | .loginBox { 81 | max-width: 90%; 82 | width: 500px 83 | } 84 | 85 | .stretch input { 86 | width: 100% 87 | } 88 | 89 | .mainBtn { 90 | width: 100%; 91 | height: 45px; 92 | } 93 | 94 | .userMenu { 95 | width: 45px; 96 | height: 45px; 97 | border-radius: 50%; 98 | background-position: center center; 99 | background-size: cover; 100 | display: block; 101 | margin: 0 auto; 102 | } 103 | 104 | .userProfile { 105 | width: 200px; 106 | height: 200px; 107 | border-radius: 50%; 108 | background-position: center center; 109 | background-size: cover; 110 | display: block; 111 | margin: 0 auto; 112 | } 113 | 114 | .test { 115 | width: 32%; 116 | height: 20px; 117 | display: inline-block; 118 | background-color: #8a6d3b; 119 | } 120 | 121 | .userProfileName { 122 | font-weight: bold; 123 | font-size: 20px; 124 | height: 100%; 125 | text-align: center; 126 | padding-top: 20px; 127 | padding-bottom: 20px; 128 | } 129 | 130 | .profileCard { 131 | height: 820px; 132 | border-radius: 5px; 133 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.19); 134 | margin-bottom: 30px; 135 | } 136 | 137 | .profileTitle { 138 | width: 100%; 139 | background-color: #2f5f8f; 140 | color: whitesmoke; 141 | padding-bottom: 25px; 142 | } 143 | 144 | .profileImageBtn { 145 | margin-right: 20px; 146 | color:white; 147 | padding-bottom: 24px; 148 | padding-right: 24px; 149 | border: 2px solid white; 150 | border-radius: 50% 151 | } 152 | 153 | .statusLabel { 154 | border-radius: 3px; 155 | width:100%; 156 | max-width: 140px; 157 | text-align: center; 158 | margin: 0px 10px; 159 | } -------------------------------------------------------------------------------- /views/translations/translation_en.json: -------------------------------------------------------------------------------- 1 | { 2 | "LANG": "Language", 3 | "PASS": "Password", 4 | "NAME": "Name", 5 | "LOGIN": "Login", 6 | "EMAIL": "email", 7 | "CHANGE": "Change", 8 | "SAVE": "Save", 9 | "CHANGE_PASS_PLACEHOLDER": "Change your password", 10 | "STATUSES": { 11 | "ALL": "All statuses", 12 | "NEW": "New", 13 | "IN_PROGRESS": "In progress", 14 | "QUESTIONS": "Questions", 15 | "DONE": "Estimated", 16 | "SENT": "Sent", 17 | "APPROVED": "Approved", 18 | "IN_DEVELOPMENT": "In development", 19 | "CLOSED": "Closed" 20 | }, 21 | "ACTIONS": { 22 | "SAVE": "Save", 23 | "DELETE": "Delete", 24 | "EDIT": "Edit", 25 | "ADD": "Add", 26 | "CANCEL": "Cancel", 27 | "BACK": "Back" 28 | }, 29 | "ACTION_MSG": { 30 | "CONFIRM_DELETE": "Confirm delete", 31 | "CONFIRM_ESTIMATION_DELETE": "Confirm estimation deletion", 32 | "CONFIRM_PROJECT_DELETE": "Confirm project deletion" 33 | }, 34 | "PROJECTS": { 35 | "PROJECTS": "Projects", 36 | "ADD": { 37 | "TITLE": "Project creation", 38 | "KEY": "Project key", 39 | "NAME": "Project name", 40 | "ESTIMATION_MODEL": "Estimation model", 41 | "ESTIMATION_TAKE_ACCOUNT": "Take account of time to estimation", 42 | "ESTIMATION_FIELDS": "Estimation fields", 43 | "ESTIMATION_FIELDS_SUBTITLE": "Can be changed for each estimation during creation or edit", 44 | "MANAGMENT_SECTIONS": "Managment sections", 45 | "SECTION_NAME": "Section name", 46 | "SECTION_PERSENT": "Percentage of development", 47 | "ITS": "Issue tracking system link", 48 | "MESSAGES": { 49 | "NEED_PROJECT_KEY": "Need project key.", 50 | "NEED_PROJECT_NAME": "Need project name.", 51 | "FIELDS_PLACEHOLDER": "Enter field name", 52 | "FIELD": "Field" 53 | } 54 | }, 55 | "MSGS": { 56 | "SAVED": "Project saved", 57 | "DELETED": "Project deleted" 58 | }, 59 | "EDIT": { 60 | "TITLE": "Project modification" 61 | } 62 | }, 63 | "ESTIMATIONS": { 64 | "COLUMNS": "Сolumns", 65 | "NO_ESTIMATIONS": "Project doesn't have estimations", 66 | "ACTIONS": "Actions", 67 | "STATUS": "Status", 68 | "KEY": "Key", 69 | "SUMMARY": "Summary", 70 | "ITS": "Issue tracking system link", 71 | "ESTIMATION_TIME": "Estimation time", 72 | "DEVELOPMENT_TIME": "Development time", 73 | "TOTAL_TIME": "Total time", 74 | "COMPONENT": "Component", 75 | "VERSION": "Version", 76 | "APPROVED_DATE": "Approved date", 77 | "WORKSTART_DATE": "work stated", 78 | "WORKEND_DATE": "work ended", 79 | "PROJECT_KEY": "Project key", 80 | "HIDE_DONE": "Hide done", 81 | "ESTIMATION_MODEL": "Estimation model", 82 | "ESTIMATION_TAKE_ACCOUNT": "Take account of time to estimation", 83 | "FIELDS": "Estimation fields", 84 | "FIELDS_PLACEHOLDER": "Enter field name", 85 | "FIELD": "Field", 86 | "CREATE_MSG": "Create estimation", 87 | "EDIT_MSG": "Edit estimation", 88 | "TICKET_LINK": "Link to ticket", 89 | "ANALYSIS_SECTION_HEADER": "Analysis and estimation", 90 | "TOTAL": "Total", 91 | "ITEM_NO": "#", 92 | "SUBITEM_NO": "#.#", 93 | "ACTIONS": { 94 | "TITLE": "Actions", 95 | "ADD_SECTION": "Add section", 96 | "DELETE_SECTION": "Delete section", 97 | "ADD_SUBSECTION": "Add sub-section", 98 | "DELETE_SUBSECTION": "Delete sub-section" 99 | }, 100 | "COMMENTS": { 101 | "TITLE": "Comments", 102 | "NEW_COMMENT": "New comment" 103 | }, 104 | "MSGS": { 105 | "SAVED": "Estimation saved", 106 | "ADDED": "Estimation added", 107 | "STATUS_CHANGED": "Status changed", 108 | "DELETED": "Estimation deleted", 109 | "LAST_SECTION": "Cannot delete last section", 110 | "LAST_SUBSECTION": "Cannot delete last sub-section" 111 | } 112 | }, 113 | "USER": { 114 | "PROFILE": "Profile", 115 | "LOGOUT": "Logout", 116 | "MSGS": { 117 | "LOGOUT": "Welcome!", 118 | "PROFILE_CHANGED": "Profile changed" 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /views/translations/translation_ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "LANG": "Язык", 3 | "PASS": "Пароль", 4 | "NAME": "Имя", 5 | "LOGIN": "Логин", 6 | "EMAIL": "Почта", 7 | "CHANGE": "Обновить", 8 | "SAVE": "Сохранить", 9 | "CHANGE_PASS_PLACEHOLDER": "Измените свой пароль", 10 | "STATUSES": { 11 | "ALL": "Все статусы", 12 | "NEW": "новая", 13 | "IN_PROGRESS": "В работе", 14 | "QUESTIONS": "Есть вопросы", 15 | "DONE": "Оценена", 16 | "SENT": "Выслана", 17 | "APPROVED": "Согласована", 18 | "IN_DEVELOPMENT": "В разработке", 19 | "CLOSED": "Закрыта" 20 | }, 21 | "ACTIONS": { 22 | "SAVE": "Сохранить", 23 | "DELETE": "Удалить", 24 | "EDIT": "Редактировать", 25 | "ADD": "Добавить", 26 | "CANCEL": "Отмена", 27 | "BACK": "Назад" 28 | }, 29 | "ACTION_MSG": { 30 | "CONFIRM_DELETE": "Подтвердите удаление", 31 | "CONFIRM_ESTIMATION_DELETE": "Подтвердите удаление эстимации", 32 | "CONFIRM_PROJECT_DELETE": "Подтвердите удаление проекта" 33 | }, 34 | "PROJECTS": { 35 | "PROJECTS": "Проекты", 36 | "ADD": { 37 | "TITLE": "Создание проекта", 38 | "KEY": "Ключ проекта", 39 | "NAME": "Название проекта", 40 | "ESTIMATION_MODEL": "Модель эстимации", 41 | "ESTIMATION_TAKE_ACCOUNT": "Учитывание трудозатрат на оценку", 42 | "ESTIMATION_FIELDS": "Поля эстимации", 43 | "ESTIMATION_FIELDS_SUBTITLE": "Можно будет изменить для каждой эстимации в проекте при создании или редактировании", 44 | "MANAGMENT_SECTIONS": "Секции мэнеджмента", 45 | "SECTION_NAME": "Нахвание секции", 46 | "SECTION_PERSENT": "Процент от разработки", 47 | "ITS": "Ссылка на баг-трекер", 48 | "MESSAGES": { 49 | "NEED_PROJECT_KEY": "Нужен ключ проекта.", 50 | "NEED_PROJECT_NAME": "Нужно имя проекта.", 51 | "FIELDS_PLACEHOLDER": "Введите имя поля", 52 | "FIELD": "Поле" 53 | } 54 | }, 55 | "MSGS": { 56 | "SAVED": "Проект сохранён", 57 | "DELETED": "Проект удалён" 58 | }, 59 | "EDIT": { 60 | "TITLE": "Редактирование проекта" 61 | } 62 | }, 63 | "ESTIMATIONS": { 64 | "COLUMNS": "Колонки", 65 | "NO_ESTIMATIONS": "У проекта пока что нет оценнок", 66 | "ACTIONS": "Действия", 67 | "STATUS": "Статус", 68 | "KEY": "Ключ", 69 | "SUMMARY": "Описание", 70 | "ITS": "Ссылка на баг-трекер", 71 | "ESTIMATION_TIME": "Время на оценку", 72 | "DEVELOPMENT_TIME": "Время на разработку", 73 | "TOTAL_TIME": "Общее время", 74 | "COMPONENT": "Компонент", 75 | "VERSION": "Версия", 76 | "APPROVED_DATE": "Дата согласования", 77 | "WORKSTART_DATE": "Начало разработки", 78 | "WORKEND_DATE": "Конец разработки", 79 | "PROJECT_KEY": "Ключ проекта", 80 | "HIDE_DONE": "Скрыть выполненные", 81 | "ESTIMATION_MODEL": "Модель эстимации", 82 | "ESTIMATION_TAKE_ACCOUNT": "Учитывание трудозатрат на оценку", 83 | "FIELDS": "Поля эстимации", 84 | "FIELDS_PLACEHOLDER": "Введите имя поля", 85 | "FIELD": "Поле", 86 | "CREATE_MSG": "Создание оценки", 87 | "EDIT_MSG": "Редактирование оценки", 88 | "TICKET_LINK": "Ссылка на тикет", 89 | "ANALYSIS_SECTION_HEADER": "Анализ и оценка", 90 | "TOTAL": "Сумма", 91 | "ITEM_NO": "№П", 92 | "SUBITEM_NO": "№П/П", 93 | "ACTIONS": { 94 | "TITLE": "Действия", 95 | "ADD_SECTION": "Добавить секцию", 96 | "DELETE_SECTION": "Удалить секцию", 97 | "ADD_SUBSECTION": "Добавить под-секцию", 98 | "DELETE_SUBSECTION": "Удалить под-секцию" 99 | }, 100 | "COMMENTS": { 101 | "TITLE": "Комментарии", 102 | "NEW_COMMENT": "Новый комментарий" 103 | }, 104 | "MSGS": { 105 | "SAVED": "Эстимация сохранена", 106 | "ADDED": "Эстимация добавлена", 107 | "STATUS_CHANGED": "Статус изменён", 108 | "DELETED": "Эстимация удалена", 109 | "LAST_SECTION": "Нельзя удалить последнюю секцию", 110 | "LAST_SUBSECTION": "Нельзя удалить последнюю под-секцию" 111 | } 112 | }, 113 | "USER": { 114 | "PROFILE": "Профиль", 115 | "LOGOUT": "Выйти", 116 | "MSGS": { 117 | "LOGOUT": "Заходите ещё!", 118 | "PROFILE_CHANGED": "Профайл обновлен" 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /views/views/pages/estimation/estimation.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{translation.ESTIMATIONS.STATUS}}: 5 | 6 | {{translation.STATUSES[estimation.status.value]}} 7 | 8 |
9 | 12 | 13 | 14 | edit 15 | 16 | 17 | 18 |
20 | 22 | {{translation.STATUSES[status.value]}} 23 | 24 |
25 |
26 |
27 |
28 | 29 | 31 | comment 32 | 33 |
34 | 35 | 36 | Об оценке 37 | 38 | {{translation.ESTIMATIONS.TICKET_LINK}}:{{estimation.key}} 40 | 41 | 42 | Время на разработку:{{estimation.developmentTime}} 43 | 44 | 45 | 46 |
47 | 48 |
49 |
{{estimation.summary}}
50 |
51 | 52 |
53 |
54 | 55 |
56 |
{{translation.ESTIMATIONS.ITEM_NO}}
57 |
{{translation.ESTIMATIONS.SUBITEM_NO}}
58 |
{{translation.ESTIMATIONS.SUMMARY}}
59 |
{{model}}
60 |
{{translation.ESTIMATIONS.TOTAL}}
61 |
62 | 63 |
64 |
65 |
 
66 |
{{translation.ESTIMATIONS.ANALYSIS_SECTION_HEADER}}
67 |
 
68 |
{{estimation.analysis.total()}}
69 |
70 | 71 |
73 |
74 |
{{$index + 1}}
75 |
76 |
 
77 |
83 |
84 |
85 |
86 | 87 |
88 |
{{section.number}}
89 |
 
90 |
91 |
 
92 |
{{section.total();}}
93 |
94 | 95 | 96 |
97 |
98 |
{{sub.subNum}}
99 |
100 |
107 |
{{sub.total();}}
108 |
109 |
110 | 111 | 112 |
113 | 114 | 115 |
116 |
{{estimation.mngmtSection.number}}
117 |
 
118 |
{{estimation.mngmtSection.name}}
119 |
 
120 |
{{estimation.mngmtSection.total()}}
121 |
122 | 123 | 124 |
125 |
126 |
{{sub.subNum}}
127 |
{{sub.descr}}
128 |
 
129 |
{{sub.estimation()}}
130 |
131 | 132 |
133 |
 
134 |
 
135 |
{{translation.ESTIMATIONS.TOTAL}}
136 |
 
137 |
{{estimation.total();}}
138 |
139 |
140 |
141 |
142 | 143 |
144 | Сохранить 145 | Сохранить и выйти 146 | 147 | Печать 148 |
149 | 150 | 151 | 152 | 153 |

{{translation.ESTIMATIONS.COMMENTS.TITLE}}

154 |
155 | 156 |
157 |
158 |
159 | 160 | 161 | 162 | 163 | 165 | send 166 | 167 |
168 | 169 |
170 | 171 |
-------------------------------------------------------------------------------- /views/views/pages/estimation/estimationAdd.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | {{translation.ESTIMATIONS.CREATE_MSG}}: 8 | {{translation.ESTIMATIONS.EDIT_MSG}}: 9 | 10 | 11 | 12 | 13 |
14 |
Нужен JIRA-ключ
15 |
16 |
17 | 18 | 19 | 20 | 21 |
22 |
Нужно описание
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |

{{translation.ESTIMATIONS.ESTIMATION_MODEL}}:

43 |
44 | 45 | 46 | {{translation.ESTIMATIONS.ESTIMATION_TAKE_ACCOUNT}} 47 | 48 | 49 |

{{translation.ESTIMATIONS.FIELDS}}:

50 | 57 | 58 | {{$chip}} 59 | 60 | 63 | 64 | 70 |
71 |
72 |
-------------------------------------------------------------------------------- /views/views/pages/estimation/estimations.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 5 | add 6 | 7 | 8 |
9 | {{translation.ESTIMATIONS.NO_ESTIMATIONS}} 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | {{ translation.STATUSES[item.value]}} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {{translation.ESTIMATIONS.HIDE_DONE}} 28 | 29 |
30 |
31 |
32 | 33 | 34 | 35 | {{ translation.ESTIMATIONS[field.fieldKey] }} 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 67 | 92 | 93 | 94 |
{{translation.ESTIMATIONS.KEY}}{{translation.ESTIMATIONS[field.fieldKey]}}{{translation.ESTIMATIONS.ACTIONS.TITLE}}
54 | {{est.key}} 55 | 57 |
58 |
59 | {{translation.STATUSES[est[field.fieldName].value]}} 60 |
61 |
62 |
63 | {{est[field.fieldName] | date : 'dd-mm-yyyy'}} 64 |
65 |
{{est[field.fieldName]}}
66 |
68 | 73 | mode_edit 75 | 76 | 77 | 87 | clear 89 | 90 | 91 |
95 |
96 |
-------------------------------------------------------------------------------- /views/views/pages/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{fromMainController}} 4 |
-------------------------------------------------------------------------------- /views/views/pages/project/projectAdd.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | {{translation.PROJECTS.ADD.TITLE}}: 7 | 8 | 9 | 10 | 11 | 12 |
13 |
{{translation.PROJECTS.ADD.MESSAGES.NEED_PROJECT_KEY}}
14 |
15 |
16 | 17 | 18 | 19 | 20 |
21 |
{{translation.PROJECTS.ADD.MESSAGES.NEED_PROJECT_NAME}}
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |

{{translation.PROJECTS.ADD.ESTIMATION_MODEL}}:

31 |
32 | 33 | 34 | {{translation.PROJECTS.ADD.ESTIMATION_TAKE_ACCOUNT}} 35 | 36 | 37 |

{{translation.PROJECTS.ADD.ESTIMATION_FIELDS}}:

38 |
{{translation.PROJECTS.ADD.ESTIMATION_FIELDS_SUBTITLE}}
39 | 40 | 47 | 48 | {{$chip}} 49 | 50 | 53 | 54 | 55 |

{{translation.PROJECTS.ADD.MANAGMENT_SECTIONS}}:

56 | 57 |
58 | 59 | 60 | 62 | 63 | 64 | 65 | 67 | 68 | 69 | 70 | {{translation.ACTIONS.DELETE}} 71 | close 72 | 73 | 74 |
75 | 76 | 78 | add 79 | 80 |
81 | 82 | 92 |
93 |
-------------------------------------------------------------------------------- /views/views/pages/project/projects.html: -------------------------------------------------------------------------------- 1 | 2 | add 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | {{project.key}} 13 | {{project.name}} 14 | 15 | 16 | 17 | 18 | {{translation.ACTIONS.DELETE}} 24 | 25 | {{translation.ACTIONS.EDIT}} 26 | 27 | 28 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /views/views/pages/user/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

{{modes[mode]}}

5 |
6 | 7 |
8 | 9 | 10 | 11 | 12 |
13 |
Скажите нам как вас зовут 14 |
15 |
16 |
17 | 18 | 19 | 20 |
21 |
Скажите нам ваш пароль. Мы никому его не расскажем 22 |
23 |
24 |
25 |
26 | Войти 27 |
28 |
29 | Регистрация 30 |
31 | Забыли пароль? 32 | 33 |
34 | 35 |
36 |
37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 |
46 | Изменить пароль 47 | 48 |
49 |
50 | Регистрация 51 |
52 | Войти 53 |
54 |
55 |
56 | 57 | 58 |
59 | 60 | 61 | 62 | 63 |
64 |
Нам нужно это поле!
65 |
66 |
67 | 68 | 69 | 70 |
71 |
Нам нужно это поле!
72 |
73 |
74 | 75 | 76 | 77 |
78 |
Нам нужно это поле!
79 |
80 |
81 | 82 | 83 | 84 |
85 |
Нам нужно это поле!
86 |
87 |
88 | 89 | 90 | 92 |
93 |
Нам нужно это поле!
94 |
Вы неверно ввели подтверждение пароля!
95 |
96 |
97 |
98 | Зарегистрироваться 99 |
100 |
101 | Забыли пароль? 102 | 103 |
104 | Войти 105 |
106 |
107 |
108 |
109 |
-------------------------------------------------------------------------------- /views/views/pages/user/profile.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |
{{user.name}}
7 |
8 |
9 |
10 |
11 |
13 |
14 |
16 |
17 |
18 | 19 |
20 |
21 |
22 | 23 | 25 | Изменить картинку профиля 26 | 28 | mode_edit 29 | 30 | 31 | 32 | Подтвердить 33 | 35 | check 36 | 37 | 38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | {{ item.value }} 64 | 65 | 66 | 67 |
68 |
69 | {{translation.CHANGE}} 70 |
71 |
72 |
73 |
-------------------------------------------------------------------------------- /views/views/popups/confirmPopup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

{{params.confirmTitle}}

5 | 6 | 7 | close 8 | 9 |
10 |
11 | 12 |
{{params.confirmText}}
13 |
14 | 15 | {{buttons.confirmButton}} 16 | {{buttons.cancelButton}} 17 | 18 |
-------------------------------------------------------------------------------- /views/views/popups/toast.html: -------------------------------------------------------------------------------- 1 | 2 | {{message}} 3 | 4 | 5 | clear 7 | 8 | -------------------------------------------------------------------------------- /views/views/templates/footer.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiraffe/estimator/cd23806180dc89e9d97b1fc8e8d14efaac48ca0c/views/views/templates/footer.html -------------------------------------------------------------------------------- /views/views/templates/header.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | Estimator 5 |

6 | 7 | {{translation.PROJECTS.PROJECTS}} 8 | 9 | 10 | 11 | 12 |
14 |
15 | 16 | 17 | {{translation.USER.PROFILE}} 18 | 19 | 20 | 21 | {{translation.USER.LOGOUT}} 22 | 23 | 24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /views/views/templates/main.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
--------------------------------------------------------------------------------