├── .meteor
├── .gitignore
├── release
├── platforms
├── .id
├── .finished-upgraders
├── packages
└── versions
├── .eslintrc
├── .eslintignore
├── .gitignore
├── packages
└── meteor-harvest-master
│ ├── harvest.js
│ ├── .gitignore
│ ├── README.md
│ ├── package.js
│ └── LICENSE
├── collections
├── utils.js
├── projects.js
├── tasks.js
├── time-entries.js
└── user.js
├── client
├── imports
│ ├── startup
│ │ └── accounts-config.js
│ └── components
│ │ ├── PieChartCard.jsx
│ │ ├── MainFrame.jsx
│ │ ├── LineChartCard.jsx
│ │ ├── Login.jsx
│ │ ├── BarChartCard.jsx
│ │ ├── BarChartRadioGroup.jsx
│ │ ├── Dashboard.jsx
│ │ ├── JiraLinker.jsx
│ │ ├── PieChart.jsx
│ │ ├── LineChart.jsx
│ │ └── BarChart.jsx
├── main.html
├── main.jsx
├── routes.jsx
└── stylesheets
│ ├── main.scss
│ └── bootstrap.css
├── server
├── methods.js
├── publications.js
├── scheduler.js
├── accounts.js
└── jira-connector.js
├── .travis.yml
├── tests
└── app-should-exist.js
└── package.json
/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.3.2.2
2 |
--------------------------------------------------------------------------------
/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb"
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .design/
2 | client/js/
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.log
3 | settings.json
4 |
--------------------------------------------------------------------------------
/packages/meteor-harvest-master/harvest.js:
--------------------------------------------------------------------------------
1 | Harvest = Npm.require('harvest');
2 |
--------------------------------------------------------------------------------
/packages/meteor-harvest-master/.gitignore:
--------------------------------------------------------------------------------
1 | .meteor/local
2 | .meteor/meteorite
3 | .npm
4 |
--------------------------------------------------------------------------------
/packages/meteor-harvest-master/README.md:
--------------------------------------------------------------------------------
1 | View the documentation at https://github.com/log0ymxm/node-harvest.
2 |
3 | The `Harvest` object is exposed via this package.
--------------------------------------------------------------------------------
/collections/utils.js:
--------------------------------------------------------------------------------
1 | Utils = {
2 | startOf (interval){
3 | return moment().startOf(interval).toDate();
4 | },
5 |
6 | endOf (interval){
7 | return moment().endOf(interval).toDate();
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/client/imports/startup/accounts-config.js:
--------------------------------------------------------------------------------
1 | import { Accounts } from 'meteor/accounts-base';
2 | import { Router, browserHistory} from 'react-router';
3 |
4 | Accounts.onLogin((user) => {
5 | browserHistory.push('/');
6 | });
7 |
--------------------------------------------------------------------------------
/client/main.html:
--------------------------------------------------------------------------------
1 |
2 | App
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/server/methods.js:
--------------------------------------------------------------------------------
1 |
2 | Meteor.startup(() => {
3 |
4 | if (Meteor.isServer){
5 |
6 |
7 | Meteor.methods({
8 |
9 | createMeteorUser: function(data) {
10 | return Accounts.createUser({email: data.user.email, fromWorker: true});
11 | }
12 | })
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - "5"
5 | before_install:
6 | - curl https://install.meteor.com | /bin/sh
7 | - export PATH="$HOME/.meteor:$PATH"
8 | - npm install -g chimp
9 | before_script:
10 | - nohup bash -c "meteor 2>&1 &" && sleep 30; cat nohup.out
11 | services:
12 | - mongodb
13 |
--------------------------------------------------------------------------------
/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | 16qrb6q13b5i615cswz8
8 |
--------------------------------------------------------------------------------
/server/publications.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 |
3 | Meteor.startup(() => {
4 |
5 | Meteor.publish('TimeEntries.all', function() {
6 | if (this.userId) {
7 | return TimeEntries.find();
8 | }
9 | return this.ready();
10 | });
11 |
12 |
13 | Meteor.publish('Users.me', function() {
14 | if (this.userId) {
15 | return this.user;
16 | }
17 | return this.ready();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/client/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { render } from 'react-dom';
4 | import {renderRoutes} from './routes.jsx';
5 | import Accounts from './imports/startup/accounts-config.js';
6 | import injectTapEventPlugin from 'react-tap-event-plugin';
7 |
8 | Meteor.startup(() => {
9 | injectTapEventPlugin();
10 | render(renderRoutes(), document.getElementById('render-target'));
11 | });
12 |
--------------------------------------------------------------------------------
/server/scheduler.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { Accounts } from 'meteor/accounts-base';
3 |
4 |
5 | Jobs = new JobCollection('jobs');
6 | Jobs.allow({ admin: () => true });
7 |
8 | Meteor.startup(() => {
9 | // If there are no users in the db, add one
10 |
11 | // Start the job queue running
12 | Jobs.startJobServer();
13 |
14 | // Create new job on startup
15 | const job = new Job(Jobs,'import', {}).save();
16 | });
17 |
--------------------------------------------------------------------------------
/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 | 1.3.0-split-minifiers-package
14 |
--------------------------------------------------------------------------------
/packages/meteor-harvest-master/package.js:
--------------------------------------------------------------------------------
1 | Package.describe({
2 | name: 'poetic:harvest-local',
3 | version: '0.1.5-rc.1',
4 | summary: 'Packaged harvest plugin.',
5 | git: 'https://github.com/poetic/meteor-harvest.git',
6 | documentation: 'README.md'
7 | });
8 |
9 | Package.onUse(function(api) {
10 | api.versionsFrom('1.2.0.2');
11 | api.addFiles('harvest.js', 'server');
12 |
13 | api.export( 'Harvest','server' );
14 | });
15 |
16 | Npm.depends({harvest: '0.1.4'});
17 |
--------------------------------------------------------------------------------
/collections/projects.js:
--------------------------------------------------------------------------------
1 | //import { SimpleSchema } from 'meteor/aldeed:simple-schema';
2 | Projects = new Mongo.Collection('projects', {idGeneration: 'MONGO'});
3 |
4 |
5 | ProjectSchema = new SimpleSchema({
6 | name: {
7 | type: String,
8 | optional: true
9 | },
10 | description: {
11 | type: String,
12 | optional: true
13 | },
14 | harvestId: {
15 | type: Number,
16 | optional: true
17 | }
18 | });
19 |
20 |
21 | Projects.attachSchema(ProjectSchema);
22 | if( Meteor.isServer ){
23 | Projects._ensureIndex({harvestId: 1});
24 | }
25 |
--------------------------------------------------------------------------------
/tests/app-should-exist.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | /* eslint-disable func-names, prefer-arrow-callback */
3 |
4 | // These are Chimp globals
5 | /* globals browser assert server */
6 |
7 | // PLEASE READ: http://guide.meteor.com/testing.html#acceptance-testing
8 | describe('app should exist', function () {
9 | beforeEach(function () {
10 | browser.url('http://localhost:3000');
11 | });
12 |
13 | it('can create a list @watch', function () {
14 | const doesExist = browser.waitForExist('render-target');
15 |
16 | assert.equal(doesExist, true);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/client/imports/components/PieChartCard.jsx:
--------------------------------------------------------------------------------
1 | import {Card, CardActions, CardHeader, CardMedia, CardTitle, CardText} from 'material-ui/Card';
2 | import React, {Component} from 'react';
3 | import { Meteor } from 'meteor/meteor';
4 | import PieChart from './PieChart.jsx';
5 |
6 | const style = {
7 | margin: '30px'
8 | }
9 |
10 | export default class PieChartCard extends Component {
11 |
12 | render() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/client/routes.jsx:
--------------------------------------------------------------------------------
1 | import { Router, Route, browserHistory, IndexRoute } from 'react-router'
2 | import React, {Component} from 'react';
3 | import MainFrame from './imports/components/MainFrame.jsx';
4 | import Login from './imports/components/Login.jsx';
5 | import Dashboard from './imports/components/Dashboard.jsx';
6 | import JiraLinker from './imports/components/JiraLinker.jsx';
7 |
8 |
9 |
10 | export const renderRoutes = () => (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/client/imports/components/MainFrame.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import getMuiTheme from 'material-ui/styles/getMuiTheme';
3 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
4 | import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme';
5 | import AppBar from 'material-ui/AppBar';
6 |
7 | const darkMuiTheme = getMuiTheme(darkBaseTheme);
8 |
9 |
10 | export default class MainFrame extends Component {
11 |
12 | render() {
13 | return (
14 |
15 |
16 |
17 |
20 | {this.props.children}
21 |
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/imports/components/LineChartCard.jsx:
--------------------------------------------------------------------------------
1 | import {Card, CardActions, CardHeader, CardMedia, CardTitle, CardText} from 'material-ui/Card';
2 | import React, {Component} from 'react';
3 | import { Meteor } from 'meteor/meteor';
4 | import LineChart from './LineChart.jsx';
5 | import { createContainer } from 'meteor/react-meteor-data';
6 |
7 | const style = {
8 | margin: '30px'
9 | }
10 |
11 | class LineChartCard extends Component {
12 |
13 | render() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 | }
26 |
27 | export default createContainer(() => {
28 | let handle = Meteor.subscribe("TimeEntries.all");
29 | return {
30 | timeEntries: TimeEntries.find().fetch(),
31 | };
32 | }, LineChartCard);
33 |
--------------------------------------------------------------------------------
/client/imports/components/Login.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import ReactDOM from 'react-dom';
4 | import { Template } from 'meteor/templating';
5 | import { Blaze } from 'meteor/blaze';
6 | import { Router, browserHistory} from 'react-router';
7 |
8 | export default class Login extends Component {
9 |
10 | constructor(props, context) {
11 | super(props);
12 | }
13 |
14 | componentDidMount() {
15 |
16 | // Use Meteor Blaze to render login buttons
17 | this.view = Blaze.render(Template.loginButtons,
18 | ReactDOM.findDOMNode(this.refs.container));
19 | }
20 |
21 | componentWillUnmount() {
22 | // Clean up Blaze view
23 | Blaze.remove(this.view);
24 | }
25 |
26 | render() {
27 | // Just render a placeholder container that will be filled in
28 | return (
29 |
30 |
31 |
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "butler",
3 | "private": true,
4 | "scripts": {
5 | "start": "meteor run",
6 | "test": "npm run lint && chimp --mocha --path=tests --browser=phantomjs",
7 | "lint": "eslint . --ext .jsx,.js",
8 | "fix": "eslint . --ext .jsx,.js --fix",
9 | "watch": "chimp --ddp=http://localhost:3000 --watch --mocha --path=tests"
10 | },
11 | "dependencies": {
12 | "highcharts": "^4.2.5",
13 | "highcharts-more": "^0.1.2",
14 | "highcharts-solid-gauge": "^0.1.2",
15 | "jira-connector": "^2.3.0",
16 | "latest-version": "^2.0.0",
17 | "lodash": "^4.11.1",
18 | "material-ui": "^0.15.0",
19 | "meteor-node-stubs": "~0.2.0",
20 | "param-store": "poetic/param-store",
21 | "react": "^15.0.1",
22 | "react-addons-css-transition-group": "^15.0.2",
23 | "react-addons-pure-render-mixin": "^15.0.2",
24 | "react-dom": "^15.0.1",
25 | "react-highcharts": "^8.3.2",
26 | "react-router": "^2.4.0",
27 | "react-tap-event-plugin": "^1.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/client/imports/components/BarChartCard.jsx:
--------------------------------------------------------------------------------
1 | import {Card, CardActions, CardHeader, CardMedia, CardTitle, CardText} from 'material-ui/Card';
2 | import React, {Component} from 'react';
3 | import { Meteor } from 'meteor/meteor';
4 | import BarChart from './BarChart.jsx';
5 | import BarChartRadioGroup from './BarChartRadioGroup.jsx';
6 |
7 | const style = {
8 | margin: '30px'
9 | }
10 |
11 | export default class BarChartCard extends Component {
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | childValue: "week"
17 | }
18 | }
19 |
20 | handleToggle(childValue) {
21 | this.setState({
22 | childValue: childValue
23 | })
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/collections/tasks.js:
--------------------------------------------------------------------------------
1 | Tasks = new Mongo.Collection("tasks",{idGeneration: 'MONGO'});
2 |
3 |
4 | TaskSchema = new SimpleSchema({
5 | name: {
6 | type: String,
7 | optional: true
8 | },
9 | billable: {
10 | type: Boolean,
11 | defaultValue: true,
12 | optional: true
13 | },
14 | harvestId: {
15 | type: Number,
16 | optional: true
17 | }
18 | });
19 |
20 | Tasks.attachSchema(TaskSchema);
21 | if( Meteor.isServer ){
22 | Tasks._ensureIndex({harvestId: 1});
23 | }
24 |
25 | Tasks.groupTimeEntriesByTask = timeEntries => {
26 | let timeEntriesByTaskId = _.groupBy(timeEntries, entry => entry.taskId);
27 |
28 | return Object.keys(timeEntriesByTaskId).map(taskId => {
29 | let timeEntriesForTask = timeEntriesByTaskId[taskId];
30 |
31 | return {
32 | taskName: Tasks.getName(taskId),
33 | duration: TimeEntries.getTotalDuration(timeEntriesForTask),
34 | };
35 | });
36 | };
37 |
38 | Tasks.getName = taskId => {
39 | return Tasks.findOne({_id: taskId}).name;
40 | };
41 |
42 | Tasks.getConcessedTaskIds = () => {
43 | return Tasks.find({name: {$regex: /concession/}}).map(task => task._id);
44 | };
45 |
--------------------------------------------------------------------------------
/packages/meteor-harvest-master/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Poetic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/client/imports/components/BarChartRadioGroup.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import {RadioButton, RadioButtonGroup} from 'material-ui/RadioButton';
4 |
5 |
6 | const style = {
7 | display: 'inline-block',
8 | width: '150px'
9 | }
10 |
11 | export default class BarChartRadioGroup extends Component {
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | period: "week"
17 | }
18 | }
19 |
20 | _toggleRadioButton(event, value) {
21 | this.props.toggle(value)
22 | this.setState({
23 | period: value
24 | })
25 | }
26 |
27 | render() {
28 | return (
29 |
30 |
35 |
40 |
45 |
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/server/accounts.js:
--------------------------------------------------------------------------------
1 | import { Accounts } from 'meteor/accounts-base';
2 |
3 | Accounts.onCreateUser((options, user) => {
4 | var harvestUser = {};
5 | if (options.fromWorker === true) {
6 | harvestUser = user;
7 | } else {
8 |
9 | // restore default behavior
10 | if (options.profile) { user.profile = options.profile }
11 |
12 | // return user being created by harvest sync
13 | if (! user.services) { return user }
14 |
15 | let service = _.keys(user.services)[0];
16 | let { email } = user.services[service];
17 | harvestUser = Meteor.users.findOne({'emails.address': email});
18 |
19 | // new employee that doesn't yet have harvest info
20 | if (! harvestUser) { throw new Meteor.Error('cannot log in', 'cannot log in') }
21 |
22 | // attach servies to existing harvest user
23 | if (! harvestUser.services) {
24 | harvestUser.services = {
25 | resume: {loginTokens: []},
26 | [`${service}`]: user.services[service],
27 | };
28 | }
29 |
30 | // remove the existing harvest user and return the user object to
31 | // let the accounts service reinsert
32 | Meteor.users.remove({_id: harvestUser._id});
33 | //return harvestUser;
34 | }
35 | return harvestUser;
36 |
37 | });
38 |
--------------------------------------------------------------------------------
/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-base # Packages every Meteor app needs to have
8 | mobile-experience # Packages for a great mobile UX
9 | mongo # The database Meteor supports right now
10 | reactive-var # Reactive variable for tracker
11 | jquery # Helpful client-side library
12 | tracker # Meteor's client-side reactive programming library
13 |
14 | standard-minifier-js # JS minifier run for production mode
15 | es5-shim # ECMAScript 5 compatibility for older browsers.
16 | ecmascript # Enable ECMAScript2015+ syntax in app code
17 |
18 | static-html
19 | aldeed:simple-schema
20 | aldeed:collection2
21 | zimme:collection-behaviours
22 | dburles:collection-helpers
23 | poetic:trello
24 | accounts-password
25 | zimme:collection-timestampable
26 | percolate:synced-cron
27 | momentjs:moment
28 | matb33:collection-hooks
29 | risul:moment-timezone
30 | meteorhacks:aggregate
31 | mikowals:batch-insert
32 | poetic:harvest-local
33 | accounts-base
34 | accounts-ui
35 | accounts-google
36 | fourseven:scss
37 | seba:minifiers-autoprefixer
38 | vsivsi:job-collection
39 | twbs:bootstrap
40 | session
41 | react-meteor-data
42 |
--------------------------------------------------------------------------------
/client/imports/components/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { createContainer } from 'meteor/react-meteor-data';
4 | import { Router, browserHistory} from 'react-router';
5 | import LineChartCard from './LineChartCard.jsx';
6 | import BarChartCard from './BarChartCard.jsx';
7 | import PieChartCard from './PieChartCard.jsx';
8 | import CircularProgress from 'material-ui/CircularProgress';
9 |
10 | class Dashboard extends Component {
11 | componentDidMount() {
12 | let { user } = this.props;
13 |
14 | if (!user){
15 | browserHistory.push('/login');
16 | } else if (user.profile.accessToken == null){
17 | browserHistory.push('/jiraLinker');
18 | }
19 | }
20 |
21 | render() {
22 | // Just render a placeholder container that will be filled in
23 | let { user } = this.props;
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
38 |
39 |
40 | );
41 | }
42 | }
43 |
44 | export default createContainer(() => {
45 | let handle = Meteor.subscribe("Users.me");
46 | return {
47 | user: Meteor.user(),
48 | };
49 | }, Dashboard);
50 |
--------------------------------------------------------------------------------
/collections/time-entries.js:
--------------------------------------------------------------------------------
1 | //import { SimpleSchema } from 'meteor/aldeed:simple-schema';
2 | TimeEntries = new Mongo.Collection("timeEntries",{idGeneration: 'MONGO'});
3 |
4 | TimeEntrySchema = new SimpleSchema({
5 | duration: {
6 | type: Number,
7 | decimal: true,
8 | defaultValue: 0,
9 | optional: true
10 | },
11 | date: {
12 | type: Date,
13 | optional: true
14 | },
15 | userId: {
16 | type: SimpleSchema.RegEx.id,
17 | optional: true
18 | },
19 | projectId: {
20 | type: SimpleSchema.RegEx.id,
21 | optional: true
22 | },
23 | taskId: {
24 | type: SimpleSchema.RegEx.id,
25 | optional: true
26 | },
27 | notes: {
28 | type: String,
29 | optional: true
30 | },
31 | harvestId: {
32 | type: Number,
33 | optional: true
34 | },
35 | harvestTrelloCardName: { // Used for trello title recovery. Probably can be removed
36 | type: String,
37 | optional: true
38 | },
39 | jiraId: {
40 | type: String,
41 | optional: true
42 | },
43 | trelloId: {
44 | type: String,
45 | optional: true
46 | }
47 | });
48 |
49 | TimeEntries.attachSchema( TimeEntrySchema );
50 | if( Meteor.isServer ){
51 | TimeEntries._ensureIndex({harvestId: 1});
52 | }
53 | TimeEntries.attachBehaviour('timestampable');
54 |
55 | TimeEntries.helpers({
56 | taskName (){
57 | return Tasks.findOne(this.taskId).name;
58 | },
59 | });
60 |
61 |
62 | TimeEntries.getTotalDuration = timeEntries => {
63 | let durations = _.compact(_.pluck(timeEntries, 'duration'));
64 |
65 | return durations.reduce((previous, current) => {
66 | return previous + current;
67 | }, 0);
68 | };
69 |
--------------------------------------------------------------------------------
/server/jira-connector.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | var JiraClient = require('jira-connector');
3 | Future = Npm.require('fibers/future');
4 |
5 | if (Meteor.isServer) {
6 |
7 | Meteor.methods({
8 | getAuthorizeURL() {
9 |
10 | var future = new Future();
11 |
12 | JiraClient.oauth_util.getAuthorizeURL({
13 | host: Meteor.settings.jira.host,
14 | oauth: {
15 | consumer_key: Meteor.settings.jira.consumerKey,
16 | private_key: Meteor.settings.jira.privateKey
17 | }
18 | }, function (error, res) {
19 | future.return(res);
20 | });
21 | return future.wait();
22 | },
23 |
24 | swapRequestTokenWithAccessToken(token, tokenSecret, oauthVerifier) {
25 |
26 | var future = new Future();
27 |
28 | JiraClient.oauth_util.swapRequestTokenWithAccessToken({
29 | host: Meteor.settings.jira.host,
30 | oauth: {
31 | token: token,
32 | token_secret: tokenSecret,
33 | oauth_verifier: oauthVerifier,
34 | consumer_key: Meteor.settings.jira.consumerKey,
35 | private_key: Meteor.settings.jira.privateKey
36 | }
37 | }, function (error, res) {
38 | future.return(res);
39 | });
40 | return future.wait();
41 | },
42 |
43 | setAccessToken(accessToken, tokenSecret,userId) {
44 | var future = new Future();
45 |
46 | Meteor.users.update({_id: userId}, {$set: {'profile.accessToken': accessToken, 'profile.tokenSecret': tokenSecret}}, function(err, res) {
47 | console.log(err);
48 |
49 | future.return(res);
50 | });
51 | return future.wait();
52 | }
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.2.7
2 | accounts-google@1.0.9
3 | accounts-oauth@1.1.12
4 | accounts-password@1.1.8
5 | accounts-ui@1.1.9
6 | accounts-ui-unstyled@1.1.12
7 | aldeed:collection2@2.9.1
8 | aldeed:collection2-core@1.1.1
9 | aldeed:schema-deny@1.0.1
10 | aldeed:schema-index@1.0.1
11 | aldeed:simple-schema@1.5.3
12 | allow-deny@1.0.4
13 | autoupdate@1.2.9
14 | babel-compiler@6.6.4
15 | babel-runtime@0.1.8
16 | base64@1.0.8
17 | binary-heap@1.0.8
18 | blaze@2.1.7
19 | blaze-tools@1.0.8
20 | boilerplate-generator@1.0.8
21 | caching-compiler@1.0.4
22 | caching-html-compiler@1.0.6
23 | callback-hook@1.0.8
24 | check@1.2.1
25 | coffeescript@1.0.17
26 | dburles:collection-helpers@1.0.4
27 | ddp@1.2.5
28 | ddp-client@1.2.6
29 | ddp-common@1.2.5
30 | ddp-rate-limiter@1.0.4
31 | ddp-server@1.2.6
32 | deps@1.0.12
33 | diff-sequence@1.0.5
34 | ecmascript@0.4.3
35 | ecmascript-runtime@0.2.10
36 | ejson@1.0.11
37 | email@1.0.12
38 | es5-shim@4.5.10
39 | fastclick@1.0.11
40 | fourseven:scss@3.4.3
41 | geojson-utils@1.0.8
42 | google@1.1.11
43 | hot-code-push@1.0.4
44 | html-tools@1.0.9
45 | htmljs@1.0.9
46 | http@1.1.5
47 | id-map@1.0.7
48 | jquery@1.11.8
49 | launch-screen@1.0.11
50 | less@2.6.0
51 | livedata@1.0.18
52 | localstorage@1.0.9
53 | logging@1.0.12
54 | matb33:collection-hooks@0.7.15
55 | mdg:validation-error@0.2.0
56 | meteor@1.1.14
57 | meteor-base@1.0.4
58 | meteorhacks:aggregate@1.3.0
59 | meteorhacks:collection-utils@1.2.0
60 | mikowals:batch-insert@1.1.13
61 | minifier-css@1.1.11
62 | minifier-js@1.1.11
63 | minimongo@1.0.15
64 | mobile-experience@1.0.4
65 | mobile-status-bar@1.0.12
66 | modules@0.6.1
67 | modules-runtime@0.6.3
68 | momentjs:moment@2.13.1
69 | mongo@1.1.7
70 | mongo-id@1.0.4
71 | mongo-livedata@1.0.12
72 | mrt:later@1.6.1
73 | npm-bcrypt@0.8.5
74 | npm-mongo@1.4.43
75 | oauth@1.1.10
76 | oauth2@1.1.9
77 | observe-sequence@1.0.11
78 | ordered-dict@1.0.7
79 | percolate:synced-cron@1.3.2
80 | poetic:harvest-local@0.1.5-rc.1
81 | poetic:trello@0.0.3
82 | promise@0.6.7
83 | raix:eventemitter@0.1.3
84 | random@1.0.9
85 | rate-limit@1.0.4
86 | react-meteor-data@0.2.9
87 | reactive-dict@1.1.7
88 | reactive-var@1.0.9
89 | reload@1.1.8
90 | retry@1.0.7
91 | risul:moment-timezone@0.5.0_5
92 | routepolicy@1.0.10
93 | seba:minifiers-autoprefixer@1.0.1
94 | service-configuration@1.0.9
95 | session@1.1.5
96 | sha@1.0.7
97 | spacebars@1.0.11
98 | spacebars-compiler@1.0.11
99 | srp@1.0.8
100 | standard-minifier-js@1.0.6
101 | static-html@1.0.7
102 | templating@1.1.9
103 | templating-tools@1.0.4
104 | tmeasday:check-npm-versions@0.2.0
105 | tracker@1.0.13
106 | twbs:bootstrap@3.3.6
107 | ui@1.0.11
108 | underscore@1.0.8
109 | url@1.0.9
110 | vsivsi:job-collection@1.3.3
111 | webapp@1.2.8
112 | webapp-hashing@1.0.9
113 | zimme:collection-behaviours@1.1.3
114 | zimme:collection-timestampable@1.0.9
115 |
--------------------------------------------------------------------------------
/client/imports/components/JiraLinker.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { Router, browserHistory} from 'react-router';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 | import TextField from 'material-ui/TextField';
6 | import CircularProgress from 'material-ui/CircularProgress';
7 |
8 | const style = {
9 | marginTop: '50px'
10 | }
11 |
12 |
13 | export default class Dashboard extends Component {
14 |
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | url: '',
19 | token: '',
20 | tokenSecret: '',
21 | oauthVerifier: '',
22 | accessToken: '',
23 | step: 1
24 | }
25 | }
26 |
27 | handleTextChange(e){
28 | this.setState({oauthVerifier: e.target.value});
29 | }
30 |
31 | getAuthorizeURL() {
32 | var _this = this;
33 | this.setState({step: 2});
34 |
35 |
36 | Meteor.call('getAuthorizeURL', function(error, res){
37 |
38 | _this.setState({
39 | url: res.url,
40 | token: res.token,
41 | tokenSecret: res.token_secret
42 | })
43 |
44 | });
45 | }
46 |
47 | swapRequestTokenWithAccessToken() {
48 | var _this = this;
49 | var token = this.state.token;
50 | var tokenSecret = this.state.tokenSecret;
51 | var oauthVerifier = this.state.oauthVerifier;
52 |
53 | Meteor.call('swapRequestTokenWithAccessToken', token, tokenSecret, oauthVerifier, function(error, res){
54 | _this.setState({
55 | accessToken: res
56 | })
57 |
58 | Meteor.call('setAccessToken', _this.state.accessToken, _this.state.tokenSecret, Meteor.userId(), function(error, res) {
59 | console.log(res);
60 | if (res) {
61 | browserHistory.push('/');
62 | }
63 | });
64 | });
65 | }
66 |
67 | renderStepOne() {
68 | if (this.state.step === 1) {
69 | return
70 | }
71 | }
72 |
73 | renderStepTwo() {
74 | if (this.state.step === 2) {
75 | if (this.state.url.length > 0) {
76 | return (
77 |
78 |
Click on THIS link and copy the authentication code in the field below
79 |
80 |
81 |
82 |
83 |
84 | )
85 | } else {
86 | return (
87 |
88 | )
89 | }
90 |
91 | }
92 | }
93 |
94 | render() {
95 | // Just render a placeholder container that will be filled in
96 | return (
97 |
98 |
99 | {this.renderStepOne()}
100 | {this.renderStepTwo()}
101 |
102 |
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/client/imports/components/PieChart.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import ReactHighcharts from 'react-highcharts';
4 | import HighchartsMore from 'highcharts-more';
5 | HighchartsMore(ReactHighcharts.Highcharts)
6 | import CircularProgress from 'material-ui/CircularProgress';
7 |
8 | export default class PieChart extends Component {
9 |
10 | getChartConfig(user) {
11 | let hours = user.totalHrsThisYear();
12 | console.log(hours);
13 | let date = new Date()
14 | let currentYear = moment(date).format("YYYY");
15 | let totalHours = Math.round((hours/1617)*100);
16 | let footer = Math.round(hours);
17 | var categories = ['Billable', 'Remaining']
18 | var data = [{
19 | y: 56.33,
20 | drilldown: {
21 | name: 'Billable Hours',
22 | categories: ['Billable', 'Remaining'],
23 | data: [hours, 1617 - hours]
24 | }
25 | }]
26 | var browserData = [],
27 | hoursData = [],i,j,
28 | dataLen = data.length,
29 | drillDataLen,
30 | brightness;
31 |
32 | // Build the data arrays
33 | for (i = 0; i < dataLen; i += 1) {
34 |
35 | // add browser data
36 | browserData.push({
37 | name: categories[i],
38 | y: data[i].y
39 | });
40 |
41 | // add version data
42 | drillDataLen = data[i].drilldown.data.length;
43 | for (j = 0; j < drillDataLen; j += 1) {
44 | hoursData.push({
45 | name: data[i].drilldown.categories[j],
46 | y: data[i].drilldown.data[j],
47 | color: j == 0 ? "#394154" : "#f3f3f3"
48 | });
49 | }
50 | }
51 |
52 | const config = {
53 | chart: {
54 | type: 'pie'
55 | },
56 | title: {
57 | text: currentYear + ' Billable Hours Progress'
58 | },
59 | subtitle: {
60 | text: totalHours + '%',
61 | align: 'center',
62 | verticalAlign: 'middle',
63 | style: {
64 | "fontSize": '30px'
65 | },
66 | y: 25
67 | },
68 | yAxis: {
69 | title: {
70 | text: 'Total percent market share'
71 | }
72 | },
73 | plotOptions: {
74 | pie: {
75 | shadow: false,
76 | center: ['50%', '50%'],
77 | states: {
78 | hover: {
79 | brightness: 0
80 | }
81 | }
82 | }
83 | },
84 | credits: {
85 | position: {
86 | align: 'center'
87 | },
88 | text: [footer]+'/1617 BILLABLE HOURS',
89 | href: null,
90 | position: {
91 | align: 'center',
92 | verticalAlign: 'bottom'
93 | },
94 | style: {
95 | fontSize: '16px',
96 | fontFamily: 'Lato',
97 | cursor: 'initial',
98 | textTransform: 'uppercase',
99 | color: '#394154',
100 | letterSpacing: '2px',
101 | fontWeight: 'bold',
102 | paddingBottom: '20px'
103 | }
104 | },
105 | series: [{
106 | name: 'Hours',
107 | data: hoursData,
108 | size: '80%',
109 | innerSize: '60%'
110 | }]
111 | }
112 | return config;
113 | }
114 |
115 | render() {
116 | let { user } = this.props;
117 | if (user) {
118 | return (
119 |
120 | )
121 | } else {
122 | return ();
123 | }
124 |
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/client/imports/components/LineChart.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import ReactHighcharts from 'react-highcharts';
3 | import HighchartsMore from 'highcharts-more';
4 | HighchartsMore(ReactHighcharts.Highcharts)
5 | import CircularProgress from 'material-ui/CircularProgress';
6 |
7 |
8 | export default class LineChart extends Component {
9 |
10 | getChartConfig(timeEntries) {
11 |
12 | const numDays = moment().isLeapYear() ? 366 : 365;
13 | const daysSoFar = moment().dayOfYear();
14 | // const timeEntries = this.props.timeEntries;
15 |
16 | // Construct The Curve
17 | let theCurve = [];
18 | let curveRunningTotal = 0;
19 | for (let i = 0; i < daysSoFar-1; i++) {
20 | curveRunningTotal += 1617 / numDays;
21 | theCurve.push([i, curveRunningTotal]);
22 | }
23 |
24 | // Construct Progress line
25 | const totalsPerDay = {};
26 |
27 | for (let i = 0; i < daysSoFar-1; i++) {
28 | totalsPerDay[i] = 0;
29 | }
30 |
31 | // For each time entry, increase duration of corresponding day entry in
32 | // totalsPerDay.
33 | timeEntries.forEach((timeEntry) => {
34 | dayOfYear = moment(timeEntry.date).dayOfYear();
35 | totalsPerDay[dayOfYear] += timeEntry.duration;
36 | console.log(timeEntry.duration);
37 | console.log(totalsPerDay);
38 | });
39 |
40 | // construct an array, which is consumed by highcharts. Create a running
41 | // total of hours worked.
42 | let dataArray = [];
43 | let runningTotal = 0;
44 | for (let prop in totalsPerDay) {
45 | if (totalsPerDay.hasOwnProperty(prop)) {
46 | runningTotal += totalsPerDay[prop];
47 | dataArray.push([parseInt(prop), runningTotal]);
48 | }
49 | }
50 |
51 | const config = {
52 | title: {
53 | text: 'PTO Tracker',
54 | x: -20 //center
55 | },
56 | plotOptions: {
57 | line: {
58 | marker: {
59 | enabled: false
60 | },
61 | },
62 | },
63 | xAxis: {
64 | title: {
65 | text: 'Day of Year',
66 | },
67 | allowDecimals: false,
68 | },
69 | yAxis: {
70 | min: 0,
71 | title: {
72 | text: 'Hours'
73 | },
74 | plotLines: [{
75 | value: 0,
76 | width: 1,
77 | color: '#808080'
78 | }],
79 | allowDecimals: false,
80 | },
81 | tooltip: {
82 | formatter: function() {
83 | const seriesName = this.point.series.name;
84 | const numHours = this.point.y.toFixed(1);
85 | const date = moment().dayOfYear(this.point.x);
86 |
87 | return `${date.format('MMM Do')}
${numHours} hours`;
88 | },
89 | },
90 | credits: {
91 | enabled: false,
92 | },
93 | // legend: {
94 | // layout: 'vertical',
95 | // align: 'right',
96 | // verticalAlign: 'middle',
97 | // borderWidth: 0
98 | // },
99 | series: [{
100 | name: 'The Curve',
101 | data: theCurve,
102 | dashStyle: 'longDash',
103 | }, {
104 | name: 'Current Progress',
105 | data: dataArray,
106 | }]
107 | };
108 | return config;
109 | }
110 |
111 | render() {
112 |
113 | let { timeEntries } = this.props;
114 | if (timeEntries) {
115 | console.log(timeEntries);
116 | return (
117 |
118 | )
119 | } else {
120 | return ();
121 | }
122 |
123 |
124 |
125 |
126 | return (
127 |
128 | )
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/client/imports/components/BarChart.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import ReactHighcharts from 'react-highcharts';
4 | import HighchartsMore from 'highcharts-more';
5 | HighchartsMore(ReactHighcharts.Highcharts)
6 | import CircularProgress from 'material-ui/CircularProgress';
7 | import {RadioButton, RadioButtonGroup} from 'material-ui/RadioButton';
8 |
9 |
10 | export default class BarChart extends Component {
11 |
12 | getChartConfig(user) {
13 | //let { user } = this.props;
14 | let billable = Math.round(user.billableHrsThisWeek());
15 | let concessed = Math.round(user.concessedHrsThisInterval('month'));
16 | let billableTimePeriod = this.props.value;
17 | // let month = Math.round(user.billableHrsThisMonth());
18 | // let quarter = Math.round(user.billableHrsThisQuarter());
19 | // console.log('month', month);
20 | // console.log('quarter', quarter);
21 |
22 | //default selection on the title
23 |
24 | //set the data according to the time period
25 | if (billableTimePeriod === 'month'){
26 | billable = Math.round(user.billableHrsThisMonth());
27 | concessed = Math.round(user.concessedHrsThisInterval('month'));
28 | } else if (billableTimePeriod === 'quarter'){
29 | billable = Math.round(user.billableHrsThisQuarter());
30 | concessed = Math.round(user.concessedHrsThisInterval('quarter'));
31 | }
32 | let max = billable + concessed;
33 |
34 | const config = {
35 | chart: {
36 | type: 'bar',
37 | },
38 | title: {
39 | text: 'You’ve logged ' + "" +[max] + ' hours this '+[billableTimePeriod]
40 | },
41 | marker: {
42 | symbol: 'circle'
43 | },
44 | legend: {
45 | symbolRadius: 10,
46 | symbolHeight: 15
47 | },
48 | xAxis: {
49 | gridLineWidth: 0,
50 | title: {
51 | text: ''
52 | },
53 | labels: {
54 | enabled: false
55 | },
56 | lineWidth: 0,
57 | minorGridLineWidth: 0,
58 | lineColor: 'transparent',
59 | minorTickLength: 0,
60 | tickLength: 0
61 | },
62 | yAxis: {
63 | max: [max],
64 | tickInterval: 1,
65 | gridLineWidth: 0,
66 | reversedStacks: false,
67 | title: {
68 | text: ''
69 | },
70 | labels: {
71 | enabled: false
72 | }
73 | },
74 | plotOptions: {
75 | series: {
76 | stacking: 'normal'
77 | }
78 | },
79 | labels: {
80 | enabled: false
81 | },
82 | credits: {
83 | enabled: false
84 | },
85 | series: [{
86 | name: 'Billable Hours',
87 | data: [billable],
88 | pointWidth: 60,
89 | color: '#394154',
90 | dataLabels: {
91 | enabled: true,
92 | color: '#FFFFFF',
93 | align: 'center',
94 | style: {
95 | fontSize: '34px',
96 | fontFamily: 'Lato, sans-serif',
97 | fontWeight: '200',
98 | textShadow: 'none'
99 | },
100 | formatter: function() {
101 | if (this.series.data[0].y != 0) {
102 | return this.series.data[0].y;
103 | } else {
104 | return null;
105 | }
106 | }
107 | }
108 | }, {
109 | name: 'Concessions',
110 | data: [concessed],
111 | pointWidth: 60,
112 | color: '#F5A623',
113 | dataLabels: {
114 | enabled: true,
115 | color: '#FFFFFF',
116 | align: 'center',
117 | style: {
118 | fontSize: '34px',
119 | fontFamily: 'Lato, sans-serif',
120 | fontWeight: '200',
121 | textShadow: 'none'
122 | },
123 | formatter: function() {
124 | if (this.series.data[0].y != 0) {
125 | return this.series.data[0].y;
126 | } else {
127 | return null;
128 | }
129 | }
130 | }
131 | }]
132 | };
133 | return config;
134 | }
135 |
136 | render() {
137 | let { user } = this.props;
138 | if (user) {
139 | return (
140 |
141 | )
142 | } else {
143 | return ();
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/collections/user.js:
--------------------------------------------------------------------------------
1 | Schema = {};
2 | /*
3 | * XXX After initial import from rails, we can remove railsId, and freshbooksId
4 | */
5 |
6 | Schema.UserProfile = new SimpleSchema({
7 | harvestId: {
8 | type: Number,
9 | optional: true
10 | },
11 | railsId: {
12 | type: Number,
13 | optional: true
14 | },
15 | freshbooksId: {
16 | type: Number,
17 | optional: true
18 | },
19 | firstName: {
20 | type: String,
21 | optional: true
22 | },
23 | lastName: {
24 | type: String,
25 | optional: true
26 | },
27 | accessToken: {
28 | type: String,
29 | optional: true
30 | },
31 | tokenSecret: {
32 | type: String,
33 | optional: true
34 | }
35 | });
36 |
37 | Schema.User = new SimpleSchema({
38 | username: {
39 | type: String,
40 | // For accounts-password, either emails or username is required, but not both. It is OK to make this
41 | // optional here because the accounts-password package does its own validation.
42 | // Third-party login packages may not require either. Adjust this schema as necessary for your usage.
43 | optional: true
44 | },
45 | emails: {
46 | type: Array,
47 | // For accounts-password, either emails or username is required, but not both. It is OK to make this
48 | // optional here because the accounts-password package does its own validation.
49 | // Third-party login packages may not require either. Adjust this schema as necessary for your usage.
50 | optional: true
51 | },
52 | "emails.$": {
53 | type: Object
54 | },
55 | "emails.$.address": {
56 | type: String,
57 | regEx: SimpleSchema.RegEx.Email
58 | },
59 | "emails.$.verified": {
60 | type: Boolean
61 | },
62 | createdAt: {
63 | type: Date,
64 | optional: true
65 | },
66 | profile: {
67 | type: Schema.UserProfile,
68 | optional: true
69 | },
70 | // Make sure this services field is in your schema if you're using any of the accounts packages
71 | services: {
72 | type: Object,
73 | optional: true,
74 | blackbox: true
75 | },
76 | // Add `roles` to your schema if you use the meteor-roles package.
77 | // Option 1: Object type
78 | // If you specify that type as Object, you must also specify the
79 | // `Roles.GLOBAL_GROUP` group whenever you add a user to a role.
80 | // Example:
81 | // Roles.addUsersToRoles(userId, ["admin"], Roles.GLOBAL_GROUP);
82 | // You can't mix and match adding with and without a group since
83 | // you will fail validation in some cases.
84 | roles: {
85 | type: Object,
86 | optional: true,
87 | blackbox: true
88 | },
89 | // Option 2: [String] type
90 | // If you are sure you will never need to use role groups, then
91 | // you can specify [String] as the type
92 | roles: {
93 | type: [String],
94 | optional: true
95 | }
96 | });
97 |
98 | Meteor.users.attachSchema(Schema.User);
99 |
100 | if( Meteor.isServer ){
101 | Meteor.users._ensureIndex({'profile.railsId': 1});
102 | Meteor.users._ensureIndex({'profile.freshbooksId': 1});
103 | Meteor.users._ensureIndex({'profile.harvestId': 1});
104 | }
105 |
106 | Meteor.users.helpers({
107 | update: function(modifier){
108 | Meteor.users.update({'_id': this._id},modifier);
109 | },
110 |
111 | totalHrsThisYear (){
112 | let entriesThisYear = TimeEntries.find({
113 | userId: this._id,
114 | date: {$gte: Utils.startOf('year'), $lte: Utils.endOf('year')}
115 | }).fetch();
116 |
117 | return TimeEntries.getTotalDuration(entriesThisYear);
118 | },
119 |
120 | // concessedHrsThisYear (){
121 | // let concessedTaskIds = Tasks.getConcessedTaskIds();
122 | // let concessedEntriesThisYear = TimeEntries.find({
123 | // userId: this._id,
124 | // date: {$gte: Utils.startOf('year'), $lte: Utils.endOf('year')},
125 | // taskId: {$in: concessedTaskIds},
126 | // }).fetch();
127 |
128 | // return TimeEntries.getTotalDuration(concessedEntriesThisYear);
129 | // },
130 |
131 | totalHrsThisWeek (){
132 | let entriesThisWeek = TimeEntries.find({
133 | userId: this._id,
134 | date: {$gte: Utils.startOf('week'), $lte: Utils.endOf('week')}
135 | }).fetch();
136 |
137 | return TimeEntries.getTotalDuration(entriesThisWeek);
138 | },
139 |
140 | // concessedHrsThisWeek (){
141 | // let concessedTaskIds = Tasks.getConcessedTaskIds();
142 | // let concessedEntriesThisWeek = TimeEntries.find({
143 | // userId: this._id,
144 | // date: {$gte: Utils.startOf('week'), $lte: Utils.endOf('week')},
145 | // taskId: {$in: concessedTaskIds},
146 | // }).fetch();
147 |
148 | // return TimeEntries.getTotalDuration(concessedEntriesThisWeek);
149 | // },
150 |
151 | totalHrsThisMonth (){
152 | let entriesThisMonth = TimeEntries.find({
153 | userId: this._id,
154 | date: {$gte: Utils.startOf('month'), $lte: Utils.endOf('month')},
155 | }).fetch();
156 |
157 | return TimeEntries.getTotalDuration(entriesThisMonth);
158 | },
159 |
160 | concessedHrsThisInterval (interval){
161 | let intervals = ['week', 'month', 'quarter', 'year'];
162 |
163 | if (! _.contains(intervals, interval)) {
164 | throw new Error('interval must be one of week, month, quarter, or year');
165 | }
166 |
167 | let concessedTaskIds = Tasks.getConcessedTaskIds();
168 | let concessedEntriesThisInterval = TimeEntries.find({
169 | userId: this._id,
170 | date: {$gte: Utils.startOf(interval), $lte: Utils.endOf(interval)},
171 | taskId: {$in: concessedTaskIds},
172 | }).fetch();
173 |
174 | return TimeEntries.getTotalDuration(concessedEntriesThisInterval);
175 | },
176 |
177 | totalHrsThisQuarter (){
178 | let entriesThisQuarter = TimeEntries.find({
179 | userId: this._id,
180 | date: {$gte: Utils.startOf('quarter'), $lte: Utils.endOf('quarter')},
181 | }).fetch();
182 |
183 | return TimeEntries.getTotalDuration(entriesThisQuarter);
184 | },
185 |
186 | billableHrsThisYear (){
187 | return this.totalHrsThisYear() - this.concessedHrsThisInterval('year');
188 | },
189 |
190 | billableHrsThisWeek (){
191 | return this.totalHrsThisWeek() - this.concessedHrsThisInterval('week');
192 | },
193 |
194 | billableHrsThisMonth (){
195 | return this.totalHrsThisMonth() - this.concessedHrsThisInterval('month');
196 | },
197 |
198 | billableHrsThisQuarter (){
199 | return this.totalHrsThisQuarter() - this.concessedHrsThisInterval('quarter');
200 | },
201 |
202 | primaryEmail (){
203 | return this.emails[0].address;
204 | },
205 |
206 | userName (){
207 | return this.services.google.given_name + " " + this.services.google.family_name;
208 | },
209 |
210 | userPicture (){
211 | return this.services.google.picture;
212 | },
213 |
214 | });
215 |
216 |
217 | Meteor.users.profile = function( userId ){
218 | var user = Meteor.users.findOne({'_id': userId});
219 | if( user ){
220 | return user.profile;
221 | }
222 | };
223 |
--------------------------------------------------------------------------------
/client/stylesheets/main.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | body{
6 | background-color: #F5F5F5;
7 | margin: 0;
8 | }
9 |
10 | .loginWrapper{
11 | background: white;
12 | width: 100%;
13 | text-align: right;
14 | padding: 20px 0;
15 | margin-bottom: 70px;
16 | -webkit-box-shadow: -1px 1px 17px -2px rgba(0,0,0,0.38);
17 | -moz-box-shadow: -1px 1px 17px -2px rgba(0,0,0,0.38);
18 | box-shadow: -1px 1px 17px -2px rgba(0,0,0,0.38);
19 | }
20 |
21 | .innerWrapper{
22 | max-width: 1024px;
23 | margin: 0 auto;
24 | padding: 0 15px;
25 | }
26 | .butler{
27 | display: inline-table;
28 | float: left;
29 | padding: 8px 10px 0 10px;
30 | img{
31 | display: inline-table;
32 | vertical-align: middle;
33 | }
34 | }
35 |
36 | .butlerText,
37 | .butlerText2{
38 | display: inline-table;
39 | font-family: 'Lato';
40 | font-weight: bold;
41 | font-size: 24px;
42 | vertical-align: middle;
43 | margin: 0;
44 | text-transform: uppercase;
45 | letter-spacing: 3px;
46 | padding-left: 20px;
47 | color: #32A5A9;
48 | }
49 |
50 | .headerLinks {
51 | font-family: "Lato";
52 | font-size: 17px;
53 | font-weight: 200;
54 | margin-left: 50px;
55 | display: inline-block;
56 | float: left;
57 | padding: 20px 0px;
58 | a {
59 | text-decoration: none;
60 | color: #32A5A9;
61 | }
62 | }
63 | .user-picture img{
64 | width: 35px;
65 | border-radius: 20px;
66 | }
67 | #login-buttons,
68 | .user-picture{
69 | display: inline-table;
70 | vertical-align: middle;
71 | }
72 | #login-buttons{
73 | padding-left: 10px;
74 | cursor: pointer;
75 | }
76 | .userWrapper{
77 | display: inline-table;
78 | margin-right: 0.2px;
79 | line-height: 1;
80 | padding: 12px 0;
81 | vertical-align: top;
82 | font-family: "Lato";
83 | font-size: 17px;
84 | font-weight: 200;
85 | position: relative;
86 | }
87 |
88 | .login-link-text{
89 | padding-right: 20px;
90 | font-family: "Lato";
91 | font-weight: 200;
92 | text-decoration: none !important;
93 | }
94 |
95 | .arrowDown{
96 | width: 15px;
97 | position: relative;
98 | left: 6px;
99 | }
100 | .logout{
101 | display: none;
102 | position: absolute;
103 | z-index: 999;
104 | background-color: white;
105 | // border: 1px red solid;
106 | top: 58px;
107 | right: 0px;
108 | padding: 0 41px;
109 | -webkit-box-shadow: -1px 1px 17px -2px rgba(0,0,0,0.38);
110 | -moz-box-shadow: -1px 1px 17px -2px rgba(0,0,0,0.38);
111 | box-shadow: -1px 1px 17px -2px rgba(0,0,0,0.38);
112 | cursor: pointer;
113 | }
114 | .btn-google{
115 | font-size: 23px;
116 | color: #32A5A9;
117 | font-family: 'Lato';
118 | text-transform: lowercase;
119 | }
120 |
121 | .activate{
122 | display: block !important;
123 | }
124 |
125 | .wrapper{
126 | width: 30%;
127 | display: inline-block;
128 | margin-right: 20px;
129 | }
130 | .wrapper2{
131 | width: 67%;
132 | display: inline-block;
133 | vertical-align: top;
134 | }
135 | .wrapper3{
136 | max-width: 1024px;
137 | margin: 0 auto;
138 | }
139 | .lineChartWrapper {
140 | display: inline-block;
141 | width: 75%;
142 | background-color: white;
143 | border: 1px #DCDCDC solid;
144 | /* height: 278px; */
145 | margin-bottom: 20px;
146 | overflow: hidden;
147 | padding: 20px 0;
148 | }
149 | .vacationHoursTracker {
150 | display: inline-block;
151 | height: 400px;
152 | margin-bottom: 20px;
153 | overflow: hidden;
154 | padding: 20px 0;
155 | padding-left: 20px;
156 | font-size: 30px !important;
157 | font-family: 'Lato';
158 | font-weight: 200;
159 | * {
160 | display: inline-block;
161 | vertical-align: middle;
162 | line-height: normal;
163 | }
164 | div {
165 | padding-top: 70px;
166 | }
167 | span {
168 | font-size: 50px;
169 | font-weight: bold;
170 | }
171 | }
172 | .hours-diff,
173 | .ahead-or-behind {
174 | font-size: 30px;
175 | display: block;
176 | }
177 | .tab-navigation{
178 | max-width: 1024px;
179 | margin: 0 auto;
180 | padding: 0 15px 20px 15px;
181 | a{
182 | font-family: 'Lato';
183 | color: black;
184 | text-decoration: none;
185 | margin-right: 20px;
186 | padding: 5px 15px;
187 | }
188 | .active{
189 | color: white;
190 | border: 1px solid #32A5A9;
191 | background: #32A5A9;
192 | cursor: default;
193 | }
194 | }
195 | .donutChartWrapper,
196 | .barChartWrapper{
197 | background-color: white;
198 | border: 1px #DCDCDC solid;
199 | height: 278px;
200 | margin-bottom: 20px;
201 | overflow: hidden;
202 | padding: 20px 0;
203 | }
204 | .barChartRadio{
205 | background-color: white;
206 | border: 1px #DCDCDC solid;
207 | margin-bottom: 20px;
208 | overflow: hidden;
209 | padding: 20px 0;
210 | }
211 | .info{
212 | clear: both;
213 | }
214 |
215 | #donut-chart .highcharts-title,
216 | #bar-chart .highcharts-title,
217 | #line-chart .highcharts-title {
218 |
219 | font-size: 30px !important;
220 | font-family: 'Lato';
221 | font-weight: 200;
222 | }
223 |
224 | #bar-chart .highcharts-title tspan:nth-child(2){
225 | font-weight: bold;
226 | }
227 |
228 | .highcharts-legend-item text{
229 | font-size: 16px !important;
230 | font-family: 'Lato';
231 | letter-spacing: 1px;
232 | text-transform: uppercase;
233 | }
234 |
235 | .radio{
236 | text-align: center;
237 | font-size: 23px;
238 | color: #32A5A9;
239 | font-family: 'Lato';
240 | text-transform: lowercase;
241 | padding: 20px;
242 | }
243 |
244 | .login-wrapper{
245 | width: 200px;
246 | margin: 150px auto;
247 | #login-sign-in-link{
248 | font-weight: 400 !important;
249 | }
250 | #login-dropdown-list{
251 | width: 185px;
252 | margin-top: 0px;
253 | }
254 | .login-close-text{
255 | font-family: 'Lato';
256 | text-decoration: none !important;
257 | }
258 | #login-buttons{
259 | #login-buttons-google{
260 | background-color: #32A5A9;
261 | padding: 10px 8px;
262 | .text-besides-image{
263 | font-size: 16px;
264 | font-family: 'Lato';
265 | margin-left: 0;
266 | }
267 | }
268 | margin-top: 15px;
269 | .login-form,
270 | .or,
271 | #login-buttons-image-google{
272 | display: none;
273 | }
274 |
275 | }
276 | }
277 | input:focus{
278 | outline: red auto;
279 | }
280 |
281 | .adminWrapper{
282 | max-width: 1024px;
283 | margin: 0 auto;
284 | .adminList{
285 | width: 30%;
286 | display: inline-table;
287 | padding: 0;
288 | padding-right: 31px;
289 | li{
290 | list-style: none;
291 | }
292 | }
293 | }
294 |
295 | @media screen and (max-width: 1000px){
296 | .donutChartWrapper svg text{
297 | font-size: 13px !important;
298 | }
299 | }
300 |
301 | @media screen and (max-width: 885px){
302 | .wrapper,
303 | .wrapper2{
304 | width: 100%;
305 | display: block;
306 | margin-right: 0;
307 | }
308 | .donutChartWrapper svg text{
309 | font-size: 16px !important;
310 | }
311 | }
312 |
313 | @media screen and (max-width: 415px){
314 | .loginWrapper{
315 | margin-bottom: 20px;
316 | padding: 0;
317 | }
318 | .butlerText{
319 | display: none;
320 | }
321 | .logout{
322 | top: 58px;
323 | }
324 | .radio{
325 | font-size: 16px;
326 | }
327 | #bar-chart .highcharts-container text{
328 | font-size: 18px !important;
329 | }
330 | }
331 |
332 | @media screen and (max-width: 740px) {
333 | .lineChartWrapper {
334 | width: 100%
335 | }
336 | .vacationHoursTracker div {
337 | padding-top: 5px;
338 | width: 100%;
339 | text-align: center;
340 | }
341 | }
342 |
--------------------------------------------------------------------------------
/client/stylesheets/bootstrap.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.3.5 (http://getbootstrap.com)
3 | * Copyright 2011-2016 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 |
7 | /*!
8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=1a7085fccf75a8bf8c5eb54e2fe90c72)
9 | * Config saved to config.json and https://gist.github.com/1a7085fccf75a8bf8c5eb54e2fe90c72
10 | */
11 | /*!
12 | * Bootstrap v3.3.6 (http://getbootstrap.com)
13 | * Copyright 2011-2015 Twitter, Inc.
14 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
15 | */
16 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
17 | html {
18 | font-family: sans-serif;
19 | -ms-text-size-adjust: 100%;
20 | -webkit-text-size-adjust: 100%;
21 | }
22 | body {
23 | margin: 0;
24 | }
25 | article,
26 | aside,
27 | details,
28 | figcaption,
29 | figure,
30 | footer,
31 | header,
32 | hgroup,
33 | main,
34 | menu,
35 | nav,
36 | section,
37 | summary {
38 | display: block;
39 | }
40 | audio,
41 | canvas,
42 | progress,
43 | video {
44 | display: inline-block;
45 | vertical-align: baseline;
46 | }
47 | audio:not([controls]) {
48 | display: none;
49 | height: 0;
50 | }
51 | [hidden],
52 | template {
53 | display: none;
54 | }
55 | a {
56 | background-color: transparent;
57 | }
58 | a:active,
59 | a:hover {
60 | outline: 0;
61 | }
62 | abbr[title] {
63 | border-bottom: 1px dotted;
64 | }
65 | b,
66 | strong {
67 | font-weight: bold;
68 | }
69 | dfn {
70 | font-style: italic;
71 | }
72 | h1 {
73 | font-size: 2em;
74 | margin: 0.67em 0;
75 | }
76 | mark {
77 | background: #ff0;
78 | color: #000;
79 | }
80 | small {
81 | font-size: 80%;
82 | }
83 | sub,
84 | sup {
85 | font-size: 75%;
86 | line-height: 0;
87 | position: relative;
88 | vertical-align: baseline;
89 | }
90 | sup {
91 | top: -0.5em;
92 | }
93 | sub {
94 | bottom: -0.25em;
95 | }
96 | img {
97 | border: 0;
98 | }
99 | svg:not(:root) {
100 | overflow: hidden;
101 | }
102 | figure {
103 | margin: 1em 40px;
104 | }
105 | hr {
106 | -webkit-box-sizing: content-box;
107 | -moz-box-sizing: content-box;
108 | box-sizing: content-box;
109 | height: 0;
110 | }
111 | pre {
112 | overflow: auto;
113 | }
114 | code,
115 | kbd,
116 | pre,
117 | samp {
118 | font-family: monospace, monospace;
119 | font-size: 1em;
120 | }
121 | button,
122 | input,
123 | optgroup,
124 | select,
125 | textarea {
126 | color: inherit;
127 | font: inherit;
128 | margin: 0;
129 | }
130 | button {
131 | overflow: visible;
132 | }
133 | button,
134 | select {
135 | text-transform: none;
136 | }
137 | button,
138 | html input[type="button"],
139 | input[type="reset"],
140 | input[type="submit"] {
141 | -webkit-appearance: button;
142 | cursor: pointer;
143 | }
144 | button[disabled],
145 | html input[disabled] {
146 | cursor: default;
147 | }
148 | button::-moz-focus-inner,
149 | input::-moz-focus-inner {
150 | border: 0;
151 | padding: 0;
152 | }
153 | input {
154 | line-height: normal;
155 | }
156 | input[type="checkbox"],
157 | input[type="radio"] {
158 | -webkit-box-sizing: border-box;
159 | -moz-box-sizing: border-box;
160 | box-sizing: border-box;
161 | padding: 0;
162 | }
163 | input[type="number"]::-webkit-inner-spin-button,
164 | input[type="number"]::-webkit-outer-spin-button {
165 | height: auto;
166 | }
167 | input[type="search"] {
168 | -webkit-appearance: textfield;
169 | -webkit-box-sizing: content-box;
170 | -moz-box-sizing: content-box;
171 | box-sizing: content-box;
172 | }
173 | input[type="search"]::-webkit-search-cancel-button,
174 | input[type="search"]::-webkit-search-decoration {
175 | -webkit-appearance: none;
176 | }
177 | fieldset {
178 | border: 1px solid #c0c0c0;
179 | margin: 0 2px;
180 | padding: 0.35em 0.625em 0.75em;
181 | }
182 | legend {
183 | border: 0;
184 | padding: 0;
185 | }
186 | textarea {
187 | overflow: auto;
188 | }
189 | optgroup {
190 | font-weight: bold;
191 | }
192 | table {
193 | border-collapse: collapse;
194 | border-spacing: 0;
195 | }
196 | td,
197 | th {
198 | padding: 0;
199 | }
200 | * {
201 | -webkit-box-sizing: border-box;
202 | -moz-box-sizing: border-box;
203 | box-sizing: border-box;
204 | }
205 | *:before,
206 | *:after {
207 | -webkit-box-sizing: border-box;
208 | -moz-box-sizing: border-box;
209 | box-sizing: border-box;
210 | }
211 | html {
212 | font-size: 10px;
213 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
214 | }
215 | body {
216 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
217 | font-size: 14px;
218 | line-height: 1.42857143;
219 | color: #333333;
220 | background-color: #ffffff;
221 | }
222 | input,
223 | button,
224 | select,
225 | textarea {
226 | font-family: inherit;
227 | font-size: inherit;
228 | line-height: inherit;
229 | }
230 | a {
231 | color: #337ab7;
232 | text-decoration: none;
233 | }
234 | a:hover,
235 | a:focus {
236 | color: #23527c;
237 | text-decoration: underline;
238 | }
239 | a:focus {
240 | outline: thin dotted;
241 | outline: 5px auto -webkit-focus-ring-color;
242 | outline-offset: -2px;
243 | }
244 | figure {
245 | margin: 0;
246 | }
247 | img {
248 | vertical-align: middle;
249 | }
250 | .img-responsive {
251 | display: block;
252 | max-width: 100%;
253 | height: auto;
254 | }
255 | .img-rounded {
256 | border-radius: 6px;
257 | }
258 | .img-thumbnail {
259 | padding: 4px;
260 | line-height: 1.42857143;
261 | background-color: #ffffff;
262 | border: 1px solid #dddddd;
263 | border-radius: 4px;
264 | -webkit-transition: all 0.2s ease-in-out;
265 | -o-transition: all 0.2s ease-in-out;
266 | transition: all 0.2s ease-in-out;
267 | display: inline-block;
268 | max-width: 100%;
269 | height: auto;
270 | }
271 | .img-circle {
272 | border-radius: 50%;
273 | }
274 | hr {
275 | margin-top: 20px;
276 | margin-bottom: 20px;
277 | border: 0;
278 | border-top: 1px solid #eeeeee;
279 | }
280 | .sr-only {
281 | position: absolute;
282 | width: 1px;
283 | height: 1px;
284 | margin: -1px;
285 | padding: 0;
286 | overflow: hidden;
287 | clip: rect(0, 0, 0, 0);
288 | border: 0;
289 | }
290 | .sr-only-focusable:active,
291 | .sr-only-focusable:focus {
292 | position: static;
293 | width: auto;
294 | height: auto;
295 | margin: 0;
296 | overflow: visible;
297 | clip: auto;
298 | }
299 | [role="button"] {
300 | cursor: pointer;
301 | }
302 | .container {
303 | margin-right: auto;
304 | margin-left: auto;
305 | padding-left: 15px;
306 | padding-right: 15px;
307 | }
308 | @media (min-width: 768px) {
309 | .container {
310 | width: 750px;
311 | }
312 | }
313 | @media (min-width: 992px) {
314 | .container {
315 | width: 970px;
316 | }
317 | }
318 | @media (min-width: 1200px) {
319 | .container {
320 | width: 1170px;
321 | }
322 | }
323 | .container-fluid {
324 | margin-right: auto;
325 | margin-left: auto;
326 | padding-left: 15px;
327 | padding-right: 15px;
328 | }
329 | .row {
330 | margin-left: -15px;
331 | margin-right: -15px;
332 | }
333 | .col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
334 | position: relative;
335 | min-height: 1px;
336 | padding-left: 15px;
337 | padding-right: 15px;
338 | }
339 | .col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
340 | float: left;
341 | }
342 | .col-xs-12 {
343 | width: 100%;
344 | }
345 | .col-xs-11 {
346 | width: 91.66666667%;
347 | }
348 | .col-xs-10 {
349 | width: 83.33333333%;
350 | }
351 | .col-xs-9 {
352 | width: 75%;
353 | }
354 | .col-xs-8 {
355 | width: 66.66666667%;
356 | }
357 | .col-xs-7 {
358 | width: 58.33333333%;
359 | }
360 | .col-xs-6 {
361 | width: 50%;
362 | }
363 | .col-xs-5 {
364 | width: 41.66666667%;
365 | }
366 | .col-xs-4 {
367 | width: 33.33333333%;
368 | }
369 | .col-xs-3 {
370 | width: 25%;
371 | }
372 | .col-xs-2 {
373 | width: 16.66666667%;
374 | }
375 | .col-xs-1 {
376 | width: 8.33333333%;
377 | }
378 | .col-xs-pull-12 {
379 | right: 100%;
380 | }
381 | .col-xs-pull-11 {
382 | right: 91.66666667%;
383 | }
384 | .col-xs-pull-10 {
385 | right: 83.33333333%;
386 | }
387 | .col-xs-pull-9 {
388 | right: 75%;
389 | }
390 | .col-xs-pull-8 {
391 | right: 66.66666667%;
392 | }
393 | .col-xs-pull-7 {
394 | right: 58.33333333%;
395 | }
396 | .col-xs-pull-6 {
397 | right: 50%;
398 | }
399 | .col-xs-pull-5 {
400 | right: 41.66666667%;
401 | }
402 | .col-xs-pull-4 {
403 | right: 33.33333333%;
404 | }
405 | .col-xs-pull-3 {
406 | right: 25%;
407 | }
408 | .col-xs-pull-2 {
409 | right: 16.66666667%;
410 | }
411 | .col-xs-pull-1 {
412 | right: 8.33333333%;
413 | }
414 | .col-xs-pull-0 {
415 | right: auto;
416 | }
417 | .col-xs-push-12 {
418 | left: 100%;
419 | }
420 | .col-xs-push-11 {
421 | left: 91.66666667%;
422 | }
423 | .col-xs-push-10 {
424 | left: 83.33333333%;
425 | }
426 | .col-xs-push-9 {
427 | left: 75%;
428 | }
429 | .col-xs-push-8 {
430 | left: 66.66666667%;
431 | }
432 | .col-xs-push-7 {
433 | left: 58.33333333%;
434 | }
435 | .col-xs-push-6 {
436 | left: 50%;
437 | }
438 | .col-xs-push-5 {
439 | left: 41.66666667%;
440 | }
441 | .col-xs-push-4 {
442 | left: 33.33333333%;
443 | }
444 | .col-xs-push-3 {
445 | left: 25%;
446 | }
447 | .col-xs-push-2 {
448 | left: 16.66666667%;
449 | }
450 | .col-xs-push-1 {
451 | left: 8.33333333%;
452 | }
453 | .col-xs-push-0 {
454 | left: auto;
455 | }
456 | .col-xs-offset-12 {
457 | margin-left: 100%;
458 | }
459 | .col-xs-offset-11 {
460 | margin-left: 91.66666667%;
461 | }
462 | .col-xs-offset-10 {
463 | margin-left: 83.33333333%;
464 | }
465 | .col-xs-offset-9 {
466 | margin-left: 75%;
467 | }
468 | .col-xs-offset-8 {
469 | margin-left: 66.66666667%;
470 | }
471 | .col-xs-offset-7 {
472 | margin-left: 58.33333333%;
473 | }
474 | .col-xs-offset-6 {
475 | margin-left: 50%;
476 | }
477 | .col-xs-offset-5 {
478 | margin-left: 41.66666667%;
479 | }
480 | .col-xs-offset-4 {
481 | margin-left: 33.33333333%;
482 | }
483 | .col-xs-offset-3 {
484 | margin-left: 25%;
485 | }
486 | .col-xs-offset-2 {
487 | margin-left: 16.66666667%;
488 | }
489 | .col-xs-offset-1 {
490 | margin-left: 8.33333333%;
491 | }
492 | .col-xs-offset-0 {
493 | margin-left: 0%;
494 | }
495 | @media (min-width: 768px) {
496 | .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
497 | float: left;
498 | }
499 | .col-sm-12 {
500 | width: 100%;
501 | }
502 | .col-sm-11 {
503 | width: 91.66666667%;
504 | }
505 | .col-sm-10 {
506 | width: 83.33333333%;
507 | }
508 | .col-sm-9 {
509 | width: 75%;
510 | }
511 | .col-sm-8 {
512 | width: 66.66666667%;
513 | }
514 | .col-sm-7 {
515 | width: 58.33333333%;
516 | }
517 | .col-sm-6 {
518 | width: 50%;
519 | }
520 | .col-sm-5 {
521 | width: 41.66666667%;
522 | }
523 | .col-sm-4 {
524 | width: 33.33333333%;
525 | }
526 | .col-sm-3 {
527 | width: 25%;
528 | }
529 | .col-sm-2 {
530 | width: 16.66666667%;
531 | }
532 | .col-sm-1 {
533 | width: 8.33333333%;
534 | }
535 | .col-sm-pull-12 {
536 | right: 100%;
537 | }
538 | .col-sm-pull-11 {
539 | right: 91.66666667%;
540 | }
541 | .col-sm-pull-10 {
542 | right: 83.33333333%;
543 | }
544 | .col-sm-pull-9 {
545 | right: 75%;
546 | }
547 | .col-sm-pull-8 {
548 | right: 66.66666667%;
549 | }
550 | .col-sm-pull-7 {
551 | right: 58.33333333%;
552 | }
553 | .col-sm-pull-6 {
554 | right: 50%;
555 | }
556 | .col-sm-pull-5 {
557 | right: 41.66666667%;
558 | }
559 | .col-sm-pull-4 {
560 | right: 33.33333333%;
561 | }
562 | .col-sm-pull-3 {
563 | right: 25%;
564 | }
565 | .col-sm-pull-2 {
566 | right: 16.66666667%;
567 | }
568 | .col-sm-pull-1 {
569 | right: 8.33333333%;
570 | }
571 | .col-sm-pull-0 {
572 | right: auto;
573 | }
574 | .col-sm-push-12 {
575 | left: 100%;
576 | }
577 | .col-sm-push-11 {
578 | left: 91.66666667%;
579 | }
580 | .col-sm-push-10 {
581 | left: 83.33333333%;
582 | }
583 | .col-sm-push-9 {
584 | left: 75%;
585 | }
586 | .col-sm-push-8 {
587 | left: 66.66666667%;
588 | }
589 | .col-sm-push-7 {
590 | left: 58.33333333%;
591 | }
592 | .col-sm-push-6 {
593 | left: 50%;
594 | }
595 | .col-sm-push-5 {
596 | left: 41.66666667%;
597 | }
598 | .col-sm-push-4 {
599 | left: 33.33333333%;
600 | }
601 | .col-sm-push-3 {
602 | left: 25%;
603 | }
604 | .col-sm-push-2 {
605 | left: 16.66666667%;
606 | }
607 | .col-sm-push-1 {
608 | left: 8.33333333%;
609 | }
610 | .col-sm-push-0 {
611 | left: auto;
612 | }
613 | .col-sm-offset-12 {
614 | margin-left: 100%;
615 | }
616 | .col-sm-offset-11 {
617 | margin-left: 91.66666667%;
618 | }
619 | .col-sm-offset-10 {
620 | margin-left: 83.33333333%;
621 | }
622 | .col-sm-offset-9 {
623 | margin-left: 75%;
624 | }
625 | .col-sm-offset-8 {
626 | margin-left: 66.66666667%;
627 | }
628 | .col-sm-offset-7 {
629 | margin-left: 58.33333333%;
630 | }
631 | .col-sm-offset-6 {
632 | margin-left: 50%;
633 | }
634 | .col-sm-offset-5 {
635 | margin-left: 41.66666667%;
636 | }
637 | .col-sm-offset-4 {
638 | margin-left: 33.33333333%;
639 | }
640 | .col-sm-offset-3 {
641 | margin-left: 25%;
642 | }
643 | .col-sm-offset-2 {
644 | margin-left: 16.66666667%;
645 | }
646 | .col-sm-offset-1 {
647 | margin-left: 8.33333333%;
648 | }
649 | .col-sm-offset-0 {
650 | margin-left: 0%;
651 | }
652 | }
653 | @media (min-width: 992px) {
654 | .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
655 | float: left;
656 | }
657 | .col-md-12 {
658 | width: 100%;
659 | }
660 | .col-md-11 {
661 | width: 91.66666667%;
662 | }
663 | .col-md-10 {
664 | width: 83.33333333%;
665 | }
666 | .col-md-9 {
667 | width: 75%;
668 | }
669 | .col-md-8 {
670 | width: 66.66666667%;
671 | }
672 | .col-md-7 {
673 | width: 58.33333333%;
674 | }
675 | .col-md-6 {
676 | width: 50%;
677 | }
678 | .col-md-5 {
679 | width: 41.66666667%;
680 | }
681 | .col-md-4 {
682 | width: 33.33333333%;
683 | }
684 | .col-md-3 {
685 | width: 25%;
686 | }
687 | .col-md-2 {
688 | width: 16.66666667%;
689 | }
690 | .col-md-1 {
691 | width: 8.33333333%;
692 | }
693 | .col-md-pull-12 {
694 | right: 100%;
695 | }
696 | .col-md-pull-11 {
697 | right: 91.66666667%;
698 | }
699 | .col-md-pull-10 {
700 | right: 83.33333333%;
701 | }
702 | .col-md-pull-9 {
703 | right: 75%;
704 | }
705 | .col-md-pull-8 {
706 | right: 66.66666667%;
707 | }
708 | .col-md-pull-7 {
709 | right: 58.33333333%;
710 | }
711 | .col-md-pull-6 {
712 | right: 50%;
713 | }
714 | .col-md-pull-5 {
715 | right: 41.66666667%;
716 | }
717 | .col-md-pull-4 {
718 | right: 33.33333333%;
719 | }
720 | .col-md-pull-3 {
721 | right: 25%;
722 | }
723 | .col-md-pull-2 {
724 | right: 16.66666667%;
725 | }
726 | .col-md-pull-1 {
727 | right: 8.33333333%;
728 | }
729 | .col-md-pull-0 {
730 | right: auto;
731 | }
732 | .col-md-push-12 {
733 | left: 100%;
734 | }
735 | .col-md-push-11 {
736 | left: 91.66666667%;
737 | }
738 | .col-md-push-10 {
739 | left: 83.33333333%;
740 | }
741 | .col-md-push-9 {
742 | left: 75%;
743 | }
744 | .col-md-push-8 {
745 | left: 66.66666667%;
746 | }
747 | .col-md-push-7 {
748 | left: 58.33333333%;
749 | }
750 | .col-md-push-6 {
751 | left: 50%;
752 | }
753 | .col-md-push-5 {
754 | left: 41.66666667%;
755 | }
756 | .col-md-push-4 {
757 | left: 33.33333333%;
758 | }
759 | .col-md-push-3 {
760 | left: 25%;
761 | }
762 | .col-md-push-2 {
763 | left: 16.66666667%;
764 | }
765 | .col-md-push-1 {
766 | left: 8.33333333%;
767 | }
768 | .col-md-push-0 {
769 | left: auto;
770 | }
771 | .col-md-offset-12 {
772 | margin-left: 100%;
773 | }
774 | .col-md-offset-11 {
775 | margin-left: 91.66666667%;
776 | }
777 | .col-md-offset-10 {
778 | margin-left: 83.33333333%;
779 | }
780 | .col-md-offset-9 {
781 | margin-left: 75%;
782 | }
783 | .col-md-offset-8 {
784 | margin-left: 66.66666667%;
785 | }
786 | .col-md-offset-7 {
787 | margin-left: 58.33333333%;
788 | }
789 | .col-md-offset-6 {
790 | margin-left: 50%;
791 | }
792 | .col-md-offset-5 {
793 | margin-left: 41.66666667%;
794 | }
795 | .col-md-offset-4 {
796 | margin-left: 33.33333333%;
797 | }
798 | .col-md-offset-3 {
799 | margin-left: 25%;
800 | }
801 | .col-md-offset-2 {
802 | margin-left: 16.66666667%;
803 | }
804 | .col-md-offset-1 {
805 | margin-left: 8.33333333%;
806 | }
807 | .col-md-offset-0 {
808 | margin-left: 0%;
809 | }
810 | }
811 | @media (min-width: 1200px) {
812 | .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
813 | float: left;
814 | }
815 | .col-lg-12 {
816 | width: 100%;
817 | }
818 | .col-lg-11 {
819 | width: 91.66666667%;
820 | }
821 | .col-lg-10 {
822 | width: 83.33333333%;
823 | }
824 | .col-lg-9 {
825 | width: 75%;
826 | }
827 | .col-lg-8 {
828 | width: 66.66666667%;
829 | }
830 | .col-lg-7 {
831 | width: 58.33333333%;
832 | }
833 | .col-lg-6 {
834 | width: 50%;
835 | }
836 | .col-lg-5 {
837 | width: 41.66666667%;
838 | }
839 | .col-lg-4 {
840 | width: 33.33333333%;
841 | }
842 | .col-lg-3 {
843 | width: 25%;
844 | }
845 | .col-lg-2 {
846 | width: 16.66666667%;
847 | }
848 | .col-lg-1 {
849 | width: 8.33333333%;
850 | }
851 | .col-lg-pull-12 {
852 | right: 100%;
853 | }
854 | .col-lg-pull-11 {
855 | right: 91.66666667%;
856 | }
857 | .col-lg-pull-10 {
858 | right: 83.33333333%;
859 | }
860 | .col-lg-pull-9 {
861 | right: 75%;
862 | }
863 | .col-lg-pull-8 {
864 | right: 66.66666667%;
865 | }
866 | .col-lg-pull-7 {
867 | right: 58.33333333%;
868 | }
869 | .col-lg-pull-6 {
870 | right: 50%;
871 | }
872 | .col-lg-pull-5 {
873 | right: 41.66666667%;
874 | }
875 | .col-lg-pull-4 {
876 | right: 33.33333333%;
877 | }
878 | .col-lg-pull-3 {
879 | right: 25%;
880 | }
881 | .col-lg-pull-2 {
882 | right: 16.66666667%;
883 | }
884 | .col-lg-pull-1 {
885 | right: 8.33333333%;
886 | }
887 | .col-lg-pull-0 {
888 | right: auto;
889 | }
890 | .col-lg-push-12 {
891 | left: 100%;
892 | }
893 | .col-lg-push-11 {
894 | left: 91.66666667%;
895 | }
896 | .col-lg-push-10 {
897 | left: 83.33333333%;
898 | }
899 | .col-lg-push-9 {
900 | left: 75%;
901 | }
902 | .col-lg-push-8 {
903 | left: 66.66666667%;
904 | }
905 | .col-lg-push-7 {
906 | left: 58.33333333%;
907 | }
908 | .col-lg-push-6 {
909 | left: 50%;
910 | }
911 | .col-lg-push-5 {
912 | left: 41.66666667%;
913 | }
914 | .col-lg-push-4 {
915 | left: 33.33333333%;
916 | }
917 | .col-lg-push-3 {
918 | left: 25%;
919 | }
920 | .col-lg-push-2 {
921 | left: 16.66666667%;
922 | }
923 | .col-lg-push-1 {
924 | left: 8.33333333%;
925 | }
926 | .col-lg-push-0 {
927 | left: auto;
928 | }
929 | .col-lg-offset-12 {
930 | margin-left: 100%;
931 | }
932 | .col-lg-offset-11 {
933 | margin-left: 91.66666667%;
934 | }
935 | .col-lg-offset-10 {
936 | margin-left: 83.33333333%;
937 | }
938 | .col-lg-offset-9 {
939 | margin-left: 75%;
940 | }
941 | .col-lg-offset-8 {
942 | margin-left: 66.66666667%;
943 | }
944 | .col-lg-offset-7 {
945 | margin-left: 58.33333333%;
946 | }
947 | .col-lg-offset-6 {
948 | margin-left: 50%;
949 | }
950 | .col-lg-offset-5 {
951 | margin-left: 41.66666667%;
952 | }
953 | .col-lg-offset-4 {
954 | margin-left: 33.33333333%;
955 | }
956 | .col-lg-offset-3 {
957 | margin-left: 25%;
958 | }
959 | .col-lg-offset-2 {
960 | margin-left: 16.66666667%;
961 | }
962 | .col-lg-offset-1 {
963 | margin-left: 8.33333333%;
964 | }
965 | .col-lg-offset-0 {
966 | margin-left: 0%;
967 | }
968 | }
969 | .clearfix:before,
970 | .clearfix:after,
971 | .container:before,
972 | .container:after,
973 | .container-fluid:before,
974 | .container-fluid:after,
975 | .row:before,
976 | .row:after {
977 | content: " ";
978 | display: table;
979 | }
980 | .clearfix:after,
981 | .container:after,
982 | .container-fluid:after,
983 | .row:after {
984 | clear: both;
985 | }
986 | .center-block {
987 | display: block;
988 | margin-left: auto;
989 | margin-right: auto;
990 | }
991 | .pull-right {
992 | float: right !important;
993 | }
994 | .pull-left {
995 | float: left !important;
996 | }
997 | .hide {
998 | display: none !important;
999 | }
1000 | .show {
1001 | display: block !important;
1002 | }
1003 | .invisible {
1004 | visibility: hidden;
1005 | }
1006 | .text-hide {
1007 | font: 0/0 a;
1008 | color: transparent;
1009 | text-shadow: none;
1010 | background-color: transparent;
1011 | border: 0;
1012 | }
1013 | .hidden {
1014 | display: none !important;
1015 | }
1016 | .affix {
1017 | position: fixed;
1018 | }
1019 | @-ms-viewport {
1020 | width: device-width;
1021 | }
1022 | .visible-xs,
1023 | .visible-sm,
1024 | .visible-md,
1025 | .visible-lg {
1026 | display: none !important;
1027 | }
1028 | .visible-xs-block,
1029 | .visible-xs-inline,
1030 | .visible-xs-inline-block,
1031 | .visible-sm-block,
1032 | .visible-sm-inline,
1033 | .visible-sm-inline-block,
1034 | .visible-md-block,
1035 | .visible-md-inline,
1036 | .visible-md-inline-block,
1037 | .visible-lg-block,
1038 | .visible-lg-inline,
1039 | .visible-lg-inline-block {
1040 | display: none !important;
1041 | }
1042 | @media (max-width: 767px) {
1043 | .visible-xs {
1044 | display: block !important;
1045 | }
1046 | table.visible-xs {
1047 | display: table !important;
1048 | }
1049 | tr.visible-xs {
1050 | display: table-row !important;
1051 | }
1052 | th.visible-xs,
1053 | td.visible-xs {
1054 | display: table-cell !important;
1055 | }
1056 | }
1057 | @media (max-width: 767px) {
1058 | .visible-xs-block {
1059 | display: block !important;
1060 | }
1061 | }
1062 | @media (max-width: 767px) {
1063 | .visible-xs-inline {
1064 | display: inline !important;
1065 | }
1066 | }
1067 | @media (max-width: 767px) {
1068 | .visible-xs-inline-block {
1069 | display: inline-block !important;
1070 | }
1071 | }
1072 | @media (min-width: 768px) and (max-width: 991px) {
1073 | .visible-sm {
1074 | display: block !important;
1075 | }
1076 | table.visible-sm {
1077 | display: table !important;
1078 | }
1079 | tr.visible-sm {
1080 | display: table-row !important;
1081 | }
1082 | th.visible-sm,
1083 | td.visible-sm {
1084 | display: table-cell !important;
1085 | }
1086 | }
1087 | @media (min-width: 768px) and (max-width: 991px) {
1088 | .visible-sm-block {
1089 | display: block !important;
1090 | }
1091 | }
1092 | @media (min-width: 768px) and (max-width: 991px) {
1093 | .visible-sm-inline {
1094 | display: inline !important;
1095 | }
1096 | }
1097 | @media (min-width: 768px) and (max-width: 991px) {
1098 | .visible-sm-inline-block {
1099 | display: inline-block !important;
1100 | }
1101 | }
1102 | @media (min-width: 992px) and (max-width: 1199px) {
1103 | .visible-md {
1104 | display: block !important;
1105 | }
1106 | table.visible-md {
1107 | display: table !important;
1108 | }
1109 | tr.visible-md {
1110 | display: table-row !important;
1111 | }
1112 | th.visible-md,
1113 | td.visible-md {
1114 | display: table-cell !important;
1115 | }
1116 | }
1117 | @media (min-width: 992px) and (max-width: 1199px) {
1118 | .visible-md-block {
1119 | display: block !important;
1120 | }
1121 | }
1122 | @media (min-width: 992px) and (max-width: 1199px) {
1123 | .visible-md-inline {
1124 | display: inline !important;
1125 | }
1126 | }
1127 | @media (min-width: 992px) and (max-width: 1199px) {
1128 | .visible-md-inline-block {
1129 | display: inline-block !important;
1130 | }
1131 | }
1132 | @media (min-width: 1200px) {
1133 | .visible-lg {
1134 | display: block !important;
1135 | }
1136 | table.visible-lg {
1137 | display: table !important;
1138 | }
1139 | tr.visible-lg {
1140 | display: table-row !important;
1141 | }
1142 | th.visible-lg,
1143 | td.visible-lg {
1144 | display: table-cell !important;
1145 | }
1146 | }
1147 | @media (min-width: 1200px) {
1148 | .visible-lg-block {
1149 | display: block !important;
1150 | }
1151 | }
1152 | @media (min-width: 1200px) {
1153 | .visible-lg-inline {
1154 | display: inline !important;
1155 | }
1156 | }
1157 | @media (min-width: 1200px) {
1158 | .visible-lg-inline-block {
1159 | display: inline-block !important;
1160 | }
1161 | }
1162 | @media (max-width: 767px) {
1163 | .hidden-xs {
1164 | display: none !important;
1165 | }
1166 | }
1167 | @media (min-width: 768px) and (max-width: 991px) {
1168 | .hidden-sm {
1169 | display: none !important;
1170 | }
1171 | }
1172 | @media (min-width: 992px) and (max-width: 1199px) {
1173 | .hidden-md {
1174 | display: none !important;
1175 | }
1176 | }
1177 | @media (min-width: 1200px) {
1178 | .hidden-lg {
1179 | display: none !important;
1180 | }
1181 | }
1182 | .visible-print {
1183 | display: none !important;
1184 | }
1185 | @media print {
1186 | .visible-print {
1187 | display: block !important;
1188 | }
1189 | table.visible-print {
1190 | display: table !important;
1191 | }
1192 | tr.visible-print {
1193 | display: table-row !important;
1194 | }
1195 | th.visible-print,
1196 | td.visible-print {
1197 | display: table-cell !important;
1198 | }
1199 | }
1200 | .visible-print-block {
1201 | display: none !important;
1202 | }
1203 | @media print {
1204 | .visible-print-block {
1205 | display: block !important;
1206 | }
1207 | }
1208 | .visible-print-inline {
1209 | display: none !important;
1210 | }
1211 | @media print {
1212 | .visible-print-inline {
1213 | display: inline !important;
1214 | }
1215 | }
1216 | .visible-print-inline-block {
1217 | display: none !important;
1218 | }
1219 | @media print {
1220 | .visible-print-inline-block {
1221 | display: inline-block !important;
1222 | }
1223 | }
1224 | @media print {
1225 | .hidden-print {
1226 | display: none !important;
1227 | }
1228 | }
1229 |
--------------------------------------------------------------------------------