├── .meteor ├── cordova-plugins ├── .gitignore ├── release ├── .id ├── .finished-upgraders ├── packages └── versions ├── client ├── lib │ ├── startup.js │ ├── clientobjects.js │ └── routechanging.js ├── views │ ├── loadingView.html │ ├── notFound.html │ ├── users │ │ ├── userManagement.css │ │ ├── login.html │ │ ├── registration.html │ │ ├── registration.js │ │ ├── userManagement.html │ │ ├── login.js │ │ └── userManagement.js │ ├── providers │ │ ├── providerList.less │ │ ├── providerEdit.js │ │ ├── providerList.html │ │ ├── providerEdit.html │ │ └── providerList.js │ ├── printouts │ │ ├── simplePrintoutGenerator.html │ │ └── printoutGenerator.html │ ├── appointments │ │ ├── sideEditWrapper.css │ │ ├── sideEditWrapper.html │ │ ├── appointmentItem.js │ │ ├── bookingTable.css │ │ ├── appointmentItem.html │ │ ├── tableItemHelpers.css │ │ ├── appointmentEdit.html │ │ ├── bookingTable.html │ │ ├── tableItemHelpers.js │ │ ├── appointmentEdit.js │ │ └── bookingTable.js │ ├── blockouts │ │ ├── blockoutItem.js │ │ ├── blockoutItem.html │ │ ├── addBlockout.html │ │ └── addBlockout.js │ ├── calendarview │ │ ├── calendar.css │ │ ├── calendar.html │ │ └── calendar.js │ ├── timepicker.html │ ├── datepicker.html │ ├── datepicker.js │ └── timepicker.js ├── booking.css ├── booking.js ├── booking.html └── routes │ └── routes.js ├── .gitignore ├── .idea ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml └── deployment.xml ├── server ├── startup.js ├── methods.js ├── publications.js └── dbrules.js ├── lib ├── blockouts.js ├── appointmentList.js ├── accounts.js ├── providers.js ├── unusualdays.js └── objects.js └── LICENSE /.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.1 2 | -------------------------------------------------------------------------------- /client/lib/startup.js: -------------------------------------------------------------------------------- 1 | AutoForm.debug(); -------------------------------------------------------------------------------- /client/views/loadingView.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/views/notFound.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /client/views/users/userManagement.css: -------------------------------------------------------------------------------- 1 | .userDeleteButton { 2 | margin-top: -7px !important; 3 | opacity: .3 !important; 4 | } -------------------------------------------------------------------------------- /client/views/providers/providerList.less: -------------------------------------------------------------------------------- 1 | .providerDeleteButton { 2 | margin-top: -7px !important; 3 | opacity: .3 !important; 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .meteor/local 2 | log 3 | tmp 4 | .meteor/meteorite 5 | .idea/workspace.xml 6 | .idea/tasks.xml 7 | .idea/deployment.xml 8 | .idea/ -------------------------------------------------------------------------------- /client/views/users/login.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/views/printouts/simplePrintoutGenerator.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/views/printouts/printoutGenerator.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /client/views/appointments/sideEditWrapper.css: -------------------------------------------------------------------------------- 1 | #editorWrapper { 2 | /*display:none; 3 | width:0;*/ 4 | transition: width 0.5s; 5 | } 6 | #insertSuccessAlert { 7 | position: absolute; 8 | } -------------------------------------------------------------------------------- /client/views/providers/providerEdit.js: -------------------------------------------------------------------------------- 1 | Template.providerEdit.helpers({ 2 | editingProvider: function(){ 3 | return providers.findOne({name: Session.get("selectedProviderName")}) 4 | } 5 | }) -------------------------------------------------------------------------------- /client/booking.css: -------------------------------------------------------------------------------- 1 | #datetimepicker1 { 2 | max-width: 100px; 3 | vertical-align: middle; 4 | display: inline-block; 5 | margin-top: 2px; 6 | border-radius: 5px; 7 | } 8 | #nextDay, #prevDay { 9 | display: inline-block; 10 | } -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /client/views/blockouts/blockoutItem.js: -------------------------------------------------------------------------------- 1 | Template.blockoutItem.helpers({ 2 | blockoutStyle: function() { 3 | return buildTableItemStyle(this); 4 | }, 5 | inbetween: function() { 6 | return inBetween(this); 7 | }, 8 | itemHighlightClass: function() { 9 | return highlightItemHelper(this); 10 | } 11 | }); -------------------------------------------------------------------------------- /client/views/calendarview/calendar.css: -------------------------------------------------------------------------------- 1 | #calendarHeader { 2 | text-align:center; 3 | width: 100%; 4 | } 5 | #calendarHeader > div { 6 | display: inline-block; 7 | } 8 | #prevMonth { 9 | float:left; 10 | } 11 | #nextMonth { 12 | float:right; 13 | } 14 | h2#month { 15 | margin-top: 0; 16 | } -------------------------------------------------------------------------------- /.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 | snkwex1lk7ktn1u3kgcr 8 | -------------------------------------------------------------------------------- /client/views/timepicker.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/views/appointments/sideEditWrapper.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/views/appointments/appointmentItem.js: -------------------------------------------------------------------------------- 1 | Template.appointmentItem.helpers({ 2 | appointmentStyle: function() { 3 | return buildTableItemStyle(this); 4 | }, 5 | inbetween: function() { 6 | return inBetween(this); 7 | }, 8 | itemHighlightClass: function() { 9 | return highlightItemHelper(this); 10 | }, 11 | time: function() { 12 | return moment(this.date).format("h:mm A"); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /client/views/blockouts/blockoutItem.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /client/views/datepicker.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/views/appointments/bookingTable.css: -------------------------------------------------------------------------------- 1 | .timeRow { 2 | height:30px; 3 | -webkit-touch-callout: none; 4 | -webkit-user-select: none; 5 | -khtml-user-select: none; 6 | -moz-user-select: none; 7 | -ms-user-select: none; 8 | user-select: none; 9 | } 10 | .rowHeader { 11 | width:6em; 12 | } 13 | #bookingTableWrapper { 14 | position:relative; 15 | } 16 | .nav-tabs { 17 | clear:both; 18 | } 19 | #deleteCustomTimes { 20 | margin-top: -10px; 21 | opacity:0.25; 22 | } 23 | #customTimeChanger { 24 | margin-top: 10px; 25 | } 26 | -------------------------------------------------------------------------------- /client/views/users/registration.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/views/providers/providerList.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/views/users/registration.js: -------------------------------------------------------------------------------- 1 | Template.registration.helpers({}) 2 | 3 | AutoForm.hooks({ 4 | registrationForm: { 5 | onSubmit: function(insertDoc, updateDoc, currentDoc) { 6 | Meteor.users.simpleSchema().clean(insertDoc); 7 | console.log(insertDoc); 8 | Accounts.createUser({ 9 | username: insertDoc.username, 10 | emails: insertDoc.emails, 11 | password: insertDoc.password, 12 | roles: insertDoc.roles 13 | }, function(error, status) { 14 | console.log(error); 15 | }); 16 | this.event.preventDefault(); 17 | return false; 18 | } 19 | } 20 | }) -------------------------------------------------------------------------------- /client/views/providers/providerEdit.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/lib/clientobjects.js: -------------------------------------------------------------------------------- 1 | closeTimeout = "";//the handle to the timeout which closes appointmentEdit after 3 seconds. 2 | //declared here for globallity. 3 | rerenderDep = new Deps.Dependency(); 4 | 5 | dayDelta = function (date) { 6 | var diff = moment(date).diff(moment().startOf('day'), "days"); 7 | if (diff===1){ 8 | return " tomorrow"; 9 | } 10 | else if (diff===-1) { 11 | return " yesterday"; 12 | } 13 | else if (diff === 0) 14 | { 15 | return " today" 16 | } 17 | else if (diff > 1) 18 | { 19 | return " in " +Math.abs(diff)+ " days" 20 | } 21 | else 22 | { 23 | return " "+Math.abs(diff)+" days ago" 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /client/views/datepicker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Mitchell on 3/12/2015. 3 | */ 4 | Template.datepicker.rendered = function() { 5 | 6 | if($("#datetimepicker1").is(":visible")) { 7 | $('#datetimepicker1').datetimepicker({ 8 | format: "YYYY-MM-DD" 9 | }); 10 | Tracker.autorun(function (comp) { 11 | try { 12 | $('#datetimepicker1').data("DateTimePicker").date(moment(Session.get("date"))); 13 | } catch (e) { 14 | //comp.invalidated = true; 15 | //comp.stopped = false; 16 | } 17 | }); 18 | $('#datetimepicker1').on("dp.change", function(e) { 19 | changeParams({date: e.date.format("YYYY-MM-DD")}); 20 | }) 21 | } 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /server/startup.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function() { 2 | Kadira.connect('dJ6gZ6qfhtsLjqt59', 'b0f959b1-3714-41d9-848f-0cdd146c5ac3'); 3 | if(Meteor.users.find().count() === 0) { 4 | var id = Accounts.createUser({ 5 | username:"admin", 6 | password:"admin", 7 | email: "admin@example.com" 8 | }); 9 | Roles.addUsersToRoles(id, "admin"); 10 | id = Accounts.createUser({ 11 | username:"booker", 12 | password:"booker", 13 | email: "booker@example.com" 14 | }); 15 | Roles.addUsersToRoles(id, "booker"); 16 | id = Accounts.createUser({ 17 | username:"provider", 18 | password:"provider", 19 | email: "provider@example.com" 20 | }); 21 | Roles.addUsersToRoles(id, "provider"); 22 | } 23 | console.log("Number of users: " + Meteor.users.find().count()) 24 | }); 25 | -------------------------------------------------------------------------------- /client/views/appointments/appointmentItem.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /server/methods.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | newUser: function () { 3 | if (Roles.userIsInRole(this.userId, 'admin')) { 4 | return Accounts.createUser({ 5 | username: "newuser", 6 | email: "fakeEmail@example.com", 7 | roles: ["booker"] 8 | // password:"newpass" 9 | }) 10 | } 11 | }, 12 | deleteUser: function (id) { 13 | if (Roles.userIsInRole(this.userId, 'admin')) { 14 | return Meteor.users.remove(id); 15 | } 16 | }, 17 | forcePassword: function (userID, pass) { 18 | if (Roles.userIsInRole(this.userId, 'admin')) { 19 | return Accounts.setPassword(userID, pass); 20 | } 21 | } 22 | }); 23 | 24 | //TODO: Define a method which removes a provider and ALL dependants 25 | -------------------------------------------------------------------------------- /.idea/deployment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /client/views/calendarview/calendar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | aldeed:autoform 7 | aldeed:collection2 8 | tsega:bootstrap3-datetimepicker 9 | meteorhacks:kadira@2.20.1 10 | less 11 | accounts-password 12 | alanning:roles 13 | underscore 14 | aldeed:simple-schema@1.0.3 15 | meteorhacks:subs-manager 16 | natestrauser:jquery-scrollto 17 | mrt:moment-timezone 18 | iron:router@1.0.0 19 | 20 | momentjs:moment 21 | twbs:bootstrap 22 | useraccounts:bootstrap 23 | multiply:iron-router-progress 24 | reactive-var 25 | standard-minifiers 26 | meteor-base 27 | mobile-experience 28 | mongo 29 | blaze-html-templates 30 | session 31 | jquery 32 | tracker 33 | logging 34 | reload 35 | random 36 | ejson 37 | spacebars 38 | check 39 | momentjs:twix 40 | rzymek:fullcalendar 41 | meteortoys:allthings 42 | -------------------------------------------------------------------------------- /client/views/appointments/tableItemHelpers.css: -------------------------------------------------------------------------------- 1 | .tableItem { 2 | position: absolute; 3 | overflow:hidden; 4 | margin-left:10px; 5 | padding-left:5px; 6 | -webkit-touch-callout: none; 7 | -webkit-user-select: none; 8 | -khtml-user-select: none; 9 | -moz-user-select: none; 10 | -ms-user-select: none; 11 | user-select: none; 12 | cursor: default; 13 | /*border: 1px inset red;*/ 14 | } 15 | .tableItemData { 16 | display: inline-block; 17 | padding-left: 5px; 18 | padding-right: 5px; 19 | } 20 | .being-edited { 21 | /*box-shadow: 0px 0px 6px 0px #428BCA;*/ 22 | background-color: #E13F33; 23 | animation: editing 2s infinite; 24 | -webkit-animation: editing 2s infinite; 25 | } 26 | 27 | @keyframes editing { 28 | 0%, 100% { box-shadow:0 0 6px 0 #E13F33; } 29 | 50% {box-shadow: 0 0 6px 5px #E13F33;} 30 | } 31 | @-webkit-keyframes editing { 32 | 0%, 100% { box-shadow:0 0 6px 0 #E13F33; } 33 | 50% {box-shadow: 0 0 6px 5px #E13F33;} 34 | } -------------------------------------------------------------------------------- /lib/blockouts.js: -------------------------------------------------------------------------------- 1 | blockouts = new Meteor.Collection('blockouts'); 2 | 3 | blockouts.attachSchema(new SimpleSchema({ 4 | title: { 5 | type: String, 6 | label: "Title", 7 | defaultValue: "Break", 8 | }, 9 | date: { 10 | type: Date, 11 | label: "Blockout Date", 12 | custom: function() { 13 | return checkDate(this, false); 14 | } 15 | }, 16 | length: { 17 | type: Number, 18 | label: "Length", 19 | min:5, 20 | defaultValue: 15, 21 | custom: function(){ 22 | if (this.value % 5 !== 0){ 23 | return "mod5"; 24 | } 25 | } 26 | }, 27 | providerName: { 28 | type: String, 29 | label: "Provider Name" 30 | } 31 | })); 32 | blockouts.simpleSchema().messages({ 33 | wtf: "What did you do to that poor date oh god", 34 | multiple: "[value] must be a multiple of 5.", 35 | overlappingDates:"That time overlaps an appointment.", 36 | overlappingBlockout:"That time overlaps another blockout.", 37 | dateOutOfBounds: "Blockout time must be within [value] o'clock." 38 | }); 39 | -------------------------------------------------------------------------------- /client/views/blockouts/addBlockout.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /client/views/providers/providerList.js: -------------------------------------------------------------------------------- 1 | Template.providerList.helpers({ 2 | providerList: function() {return providers.find();} 3 | }); 4 | Template.providerList.events({ 5 | 'click .providerName': function(event){ 6 | try { 7 | AutoForm.resetForm("updateProviderForm") 8 | } catch (e) {} 9 | Session.set("selectedProviderName", $(event.currentTarget).data("name")); 10 | }, 11 | 'click .providerDeleteButton': function(event){ 12 | if (confirm("Are you absolutely sure? This will delete *ALL* this providers data and appointments!")) 13 | { 14 | if (confirm("ALL appointments and data related to this provider will be deleted. Are you totally, complete, utterly sure?")){ 15 | providers.remove($(event.currentTarget).parent().data("id"), function(err, result) { 16 | console.log(err); 17 | console.log(result); 18 | }); 19 | } 20 | } 21 | }, 22 | 'click #newProviderButton': function(event){ 23 | providers.insert({name: "New Provider"}, function(error, id) { 24 | Session.set("selectedProviderName", providers.findOne(id).name); 25 | }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /client/views/appointments/appointmentEdit.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /client/views/users/userManagement.html: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /client/booking.js: -------------------------------------------------------------------------------- 1 | Template.navbar.events({ 2 | 'click #nextDay': function() { 3 | //if current route == calendar then change current month 4 | changeParams({date: moment(Session.get("date")).add(1, 'day').format("YYYY-MM-DD")}); 5 | }, 6 | 'click #prevDay': function() { 7 | changeParams({date: moment(Session.get("date")).subtract(1, 'day').format("YYYY-MM-DD")}); 8 | }, 9 | 'click #datetimepicker1': function() { 10 | $('#datetimepicker1').data("DateTimePicker").show() 11 | }, 12 | 'click #newAppointButton': function() { 13 | newAppointment('12:00 PM', false); 14 | }, 15 | 'click #newBlockButton': function() { 16 | newAppointment('12:00 PM', true); 17 | }, 18 | 'click #signOutButton': function() { 19 | Meteor.logout(); 20 | } 21 | }); 22 | Template.navbar.helpers({ 23 | isCalendar: function() { 24 | return Router.current().route.getName() === "calendar"; 25 | }, 26 | calendarMonth: function() { 27 | return moment(Session.get('date')).format("MMMM"); 28 | }, 29 | calendarYear: function() { 30 | return moment(Session.get('date')).format("YYYY"); 31 | }, 32 | loggedIn: function() { 33 | return Meteor.userId(); 34 | }, 35 | homeLinkDate: function() { 36 | //return moment(Session.get('date')).format('YYYY-MM-DD'); 37 | return moment().format('YYYY-MM-DD'); 38 | }, 39 | homeLinkProv: function() { 40 | var returnitem; 41 | try { 42 | returnitem = Session.get('selectedProviderName') || providers.findOne().name; 43 | }catch (e){} 44 | return returnitem; 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /client/views/users/login.js: -------------------------------------------------------------------------------- 1 | Template.loginPage.rendered = function() { 2 | }; 3 | Tracker.autorun(function() { 4 | try { 5 | if(Meteor.user() && Router.current().route.getName() == "loginPage") { 6 | Router.go('/'+Session.get('loginRedirect')) 7 | } 8 | } 9 | catch (e) {} 10 | }); 11 | 12 | AccountsTemplates.configure({ 13 | // homeRoutePath: function() { 14 | // // if(typeof Session.get("loginRedirect") !== "undefined") { 15 | // // return '/' + Session.get("loginRedirect"); 16 | // // } else { 17 | // return '/'; 18 | // // } 19 | // }, 20 | //redirectTimeout: 1000, 21 | forbidClientAccountCreation: true, 22 | showForgotPasswordLink: false 23 | }); 24 | //AccountsTemplates.configureRoute('signIn', { 25 | // redirect: function() { 26 | // if(typeof Session.get("loginRedirect") !== "undefined") { 27 | // console.log("redirecting to /" + Session.get("loginRedirect")); 28 | // Router.go('/' + Session.get("loginRedirect")); 29 | // } else { 30 | // Router.go('/'); 31 | // } 32 | // }, 33 | // name: 'login', 34 | // path: '/login/:redirect?', 35 | // template: "login" 36 | //}); 37 | // AccountsTemplates.configureRoute('forgotPwd'); 38 | AccountsTemplates.removeField('email'); 39 | AccountsTemplates.removeField('password'); 40 | AccountsTemplates.addFields([ 41 | { 42 | _id: "username", 43 | type: "text", 44 | displayName: "username", 45 | required: true, 46 | minLength: 4 47 | }, 48 | { 49 | _id: "password", 50 | type: "password", 51 | displayName: "password", 52 | required: true, 53 | minLength:4 54 | } 55 | 56 | ]); 57 | // Accounts.ui.config({ 58 | // passwordSignupFields: "USERNAME_ONLY", 59 | // }) -------------------------------------------------------------------------------- /client/views/users/userManagement.js: -------------------------------------------------------------------------------- 1 | Template.userList.helpers({ 2 | userList: function() {return Meteor.users.find()}, 3 | userIsAdmin: function() {return Roles.userIsInRole(Meteor.userId(), 'admin')} 4 | }) 5 | Template.userList.events({ 6 | 'click #newUserButton': function(event) { 7 | Meteor.call('newUser', function(result) { 8 | console.log(result); 9 | })}, 10 | 'click .userName': function(event) { 11 | Session.set("editingUser", $(event.currentTarget).data("id")) 12 | }, 13 | 'click .userDeleteButton': function (event) { 14 | if (confirm("Are you absolutely sure? This will delete *ALL* this users data!")) { 15 | if (confirm("ALL data related to this user will be deleted. Are you totally, complete, utterly sure?")){ 16 | Meteor.call('deleteUser', $(event.currentTarget).parent().data("id"), function(err, result) { 17 | console.log(err); 18 | console.log(result); 19 | }); 20 | } 21 | } 22 | }, 23 | 'click #changePasswordButton': function(event) { 24 | if (confirm("Are you sure you want to change this users password?")) { 25 | Meteor.call("forcePassword", Session.get('editingUser'), $('#changePasswordForm').val(), function(error, result) { 26 | console.log(error); 27 | console.log(result); 28 | }) 29 | } 30 | } 31 | }) 32 | Template.userEdit.rendered = function() { 33 | Session.set("editingUser", Meteor.users.findOne()._id) 34 | } 35 | 36 | Template.userEdit.helpers ({ 37 | usercoll: function() {return Meteor.users}, 38 | editingUser: function() { 39 | if(typeof Session.get("editingUser") !== "undefined") { 40 | return Meteor.users.findOne(Session.get("editingUser")); 41 | } else { 42 | return Meteor.users.findOne(); 43 | } 44 | }, 45 | isProvider: function() { 46 | return Roles.userIsInRole(Session.get("editingUser"), 'provider'); 47 | }, 48 | provOptions: function() { 49 | var ret = []; 50 | _.each(providers.find().fetch(), function(prov) { 51 | ret.push({label: prov.name, value: prov.name}); 52 | }); 53 | return ret; 54 | } 55 | }) -------------------------------------------------------------------------------- /lib/appointmentList.js: -------------------------------------------------------------------------------- 1 | appointmentList = new Meteor.Collection("appointmentList"); 2 | appointmentList.attachSchema(new SimpleSchema({ 3 | firstname: { 4 | type: String, 5 | label: "First Name", 6 | max: 100 7 | }, 8 | lastname: { 9 | type: String, 10 | label: "Last Name", 11 | max: 100 12 | }, 13 | phone: { 14 | type: String, 15 | label: "Phone Number", 16 | optional: true 17 | }, 18 | notes: { 19 | type: String, 20 | label: "Notes", 21 | optional: true 22 | }, 23 | date: { 24 | type: Date, 25 | label: "Date", 26 | index: true, 27 | custom: function() { 28 | return checkDate(this, true); 29 | } 30 | }, 31 | length: { 32 | type: Number, 33 | label: "Appointment length", 34 | min: 5, 35 | //defaultValue: function() { 36 | // var provObject = getProvObject(Session.get('date'), Session.get('selectedProviderName')); 37 | // return provObject.appointmentLength; 38 | //}, 39 | autoValue: function() { 40 | if (typeof this.value == 'undefined') { 41 | var cleanDate = moment(this.field('date').value).startOf('day').toDate(); 42 | var provObject = getProvObject(cleanDate, this.field('providerName').value); 43 | return provObject.appointmentLength; 44 | } 45 | }, 46 | custom: function(){ 47 | if (this.value % 5 !== 0){ 48 | return "multiple"; 49 | } 50 | } 51 | }, 52 | providerName: { 53 | type: String, 54 | label: "Provider Name", 55 | autoform: { 56 | omit: true 57 | } 58 | }, 59 | createdAt: { 60 | type: Date, 61 | autoform: { 62 | omit: true 63 | }, 64 | autoValue: function() { 65 | if (this.isInsert) { 66 | return new Date(); 67 | } else if (this.isUpsert) { 68 | return {$setOnInsert: new Date()}; 69 | } else { 70 | this.unset(); 71 | } 72 | } 73 | } 74 | })); 75 | appointmentList.simpleSchema().messages({ 76 | wtf: "What did you do to that poor date oh god", 77 | multiple: "[value] must be a multiple of 5.", 78 | overlappingDates:"That time overlaps another appointment.", 79 | overlappingBlockout:"That time overlaps a blockout.", 80 | dateOutOfBounds: "Appointment time must be within [value] o'clock." 81 | }); 82 | -------------------------------------------------------------------------------- /client/views/calendarview/calendar.js: -------------------------------------------------------------------------------- 1 | Template.calendar.helpers({ 2 | dateString: function() { 3 | return moment(Session.get("calendarStart")).format("MMMM YYYY"); 4 | }, 5 | prevmonth: function() { 6 | return moment(Session.get("calendarStart")).subtract(1, "month").format("MMMM") 7 | }, 8 | nextmonth: function() { 9 | return moment(Session.get("calendarStart")).add(1, "month").format("MMMM") 10 | }, 11 | }); 12 | 13 | Template.calendar.rendered = function() { 14 | var currentView = $("#innercalendar").fullCalendar("getView"); 15 | $("#innercalendar").fullCalendar('gotoDate', Session.get("calendarStart")); 16 | //$("#innercalendar").on('viewRender', function(view, element) { 17 | // console.log('jquery event triggered'); 18 | // console.log(view.start); 19 | //}) 20 | Tracker.autorun(function() { 21 | try { 22 | if (Router.current().route.getName() == "calendar") { 23 | $('#innercalendar').fullCalendar("removeEvents"); 24 | console.log("populating calendar - " + unusualDays.find().count()); 25 | var containingObject = { events: []}; 26 | _.forEach(unusualDays.find().fetch(), function(day) { 27 | var title = day.providerName; 28 | if (typeof day.notes !== "undefined") { 29 | title = title + " \n" + day.notes; 30 | } 31 | containingObject.events.push({title:title, start: day.date, allDay: true }) 32 | }); 33 | $('#innercalendar').fullCalendar("addEventSource", containingObject) 34 | } 35 | } 36 | catch (e) { 37 | console.error("caught error while populating calendar"); 38 | console.log(e); 39 | } 40 | 41 | }); 42 | 43 | }; 44 | Template.calendar.events({ 45 | 'viewRender #innercalendar': function(event, view, element) { 46 | console.log('meteor event triggered'); 47 | console.log(view.start); 48 | }, 49 | 'click div#nextMonth button': function(event) { 50 | event.stopImmediatePropagation(); 51 | var newdate = moment(Session.get("date")).add(1, "month"); 52 | Router.go("/calendar/"+newdate.format("YYYY")+"/"+newdate.format("MMMM")) 53 | }, 54 | 'click div#prevMonth button': function(event) { 55 | event.stopImmediatePropagation(); 56 | var newdate = moment(Session.get("date")).subtract(1, "month"); 57 | Router.go("/calendar/"+newdate.format("YYYY")+"/"+newdate.format("MMMM")) 58 | }, 59 | }); -------------------------------------------------------------------------------- /client/views/timepicker.js: -------------------------------------------------------------------------------- 1 | //Template["afInputTimePicker"].helpers({ 2 | //}); 3 | 4 | function addFormControlAtts() { 5 | var atts = _.clone(this.atts); 6 | if (typeof atts["class"] === "string") { 7 | atts["class"] += " form-control"; 8 | } else { 9 | atts["class"] = "form-control"; 10 | } 11 | return atts; 12 | } 13 | Template.afInputTimePicker.atts = addFormControlAtts; 14 | 15 | Template.afInputTimePicker.rendered = function() { 16 | this.$('#datetimepicker').datetimepicker({ 17 | format: "h:mm A", 18 | stepping:5 19 | }); 20 | self = this; 21 | this.autorun(function () { 22 | var data = Template.currentData(); 23 | var dtp = self.$("#datetimepicker").data("DateTimePicker"); 24 | 25 | // set field value 26 | if (data.value instanceof Date) { 27 | dtp.date(data.value); 28 | } else { 29 | dtp.date(); // clear 30 | } 31 | }); 32 | this.autorun(function(comp) { 33 | try{ 34 | if (Session.get("newTime") && Session.get("formForInsert") && 35 | (Router.current().route.getName() === "newAppointment" || 36 | Router.current().route.getName() === "newBlockout")) { 37 | console.log("setting DateTimePicker value from newTime"); 38 | self.$('#datetimepicker').data("DateTimePicker").date(moment(Session.get("newTime"), "h:mm A")) 39 | } 40 | } 41 | catch (e) { 42 | //comp.invalidated = true; 43 | //comp.stopped = false; 44 | console.log(e); 45 | } 46 | }) 47 | }; 48 | Template.afInputTimePicker.events = { 49 | 'click #datetimepicker': function (event){ 50 | $(event.currentTarget).data("DateTimePicker").show(); 51 | } 52 | }; 53 | Template.afInputTimePicker.onDestroyed = function() { 54 | $("div.bootstrap-datetimepicker-widget").remove(); 55 | }; 56 | AutoForm.addInputType("timePicker", { 57 | template: "afInputTimePicker", 58 | valueIn: function(val) { 59 | //try { 60 | // $('#datetimepicker').data("DateTimePicker").date(val); 61 | // console.log("managed to set timePicker time from ValueIn, "+ val) 62 | //} catch(e) { 63 | // console.log("failed to set timePicker time from ValueIn") 64 | // console.log(e); 65 | //} 66 | return val; 67 | }, 68 | valueOut: function() { 69 | try { 70 | return $('#datetimepicker').data("DateTimePicker").date().toDate(); 71 | } catch (e) { 72 | 73 | } 74 | } 75 | }); -------------------------------------------------------------------------------- /client/views/appointments/bookingTable.html: -------------------------------------------------------------------------------- 1 | 64 | 65 | 71 | 72 | -------------------------------------------------------------------------------- /client/booking.html: -------------------------------------------------------------------------------- 1 | 2 | Booking 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /lib/accounts.js: -------------------------------------------------------------------------------- 1 | 2 | // Meteor.users.allow({ 3 | // update: function(userId, doc, fieldNames, modifier) { 4 | // if (_.contains(Meteor.users.findOne(userId).roles, "admin")) { 5 | // return true; 6 | // } else { 7 | // return false; 8 | // } 9 | // } 10 | // }); 11 | // Accounts.validateNewUser(function (user) { 12 | // console.log(user) 13 | // try { 14 | // var loggedInUser = Meteor.user(); 15 | 16 | // if (Roles.userIsInRole(loggedInUser, ['admin'])) { 17 | // return true; 18 | // } 19 | 20 | // throw new Meteor.Error(403, "Not authorized to create new users"); 21 | // } catch (e) { 22 | // return true; 23 | // } 24 | 25 | // }); 26 | Schema = {}; 27 | Schema.UserProfile = new SimpleSchema({ 28 | firstName: { 29 | type: String, 30 | regEx: /^[a-zA-Z-]{2,25}$/, 31 | optional: true 32 | }, 33 | lastName: { 34 | type: String, 35 | regEx: /^[a-zA-Z]{2,25}$/, 36 | optional: true 37 | }, 38 | birthday: { 39 | type: Date, 40 | optional: true 41 | }, 42 | gender: { 43 | type: String, 44 | allowedValues: ['Male', 'Female'], 45 | optional: true 46 | }, 47 | organization : { 48 | type: String, 49 | regEx: /^[a-z0-9A-z .]{3,30}$/, 50 | optional: true 51 | }, 52 | }); 53 | 54 | Schema.User = new SimpleSchema({ 55 | _id: { 56 | type: String, 57 | regEx: SimpleSchema.RegEx.Id 58 | }, 59 | username: { 60 | type: String, 61 | regEx: /^[a-z0-9A-Z_]{3,15}$/, 62 | // optional: true, 63 | custom: function () { 64 | console.log(this); 65 | } 66 | }, 67 | emails: { 68 | optional: true, 69 | type: [Object], 70 | }, 71 | "emails.$.address": { 72 | optional: true, 73 | type: String, 74 | regEx: SimpleSchema.RegEx.Email 75 | }, 76 | "emails.$.verified": { 77 | optional: true, 78 | type: Boolean 79 | }, 80 | createdAt: { 81 | type: Date 82 | }, 83 | profile: { 84 | type: Schema.UserProfile, 85 | optional: true 86 | }, 87 | services: { 88 | type: Object, 89 | optional: true, 90 | blackbox: true 91 | }, 92 | // Add `roles` to your schema if you use the meteor-roles package. 93 | // Note that when using this package, you must also specify the 94 | // `Roles.GLOBAL_GROUP` group whenever you add a user to a role. 95 | // Roles.addUsersToRoles(userId, ["admin"], Roles.GLOBAL_GROUP); 96 | // You can't mix and match adding with and without a group since 97 | // you will fail validation in some cases. 98 | roles: { 99 | type: [String], 100 | optional: true, 101 | blackbox: true, 102 | allowedValues: ['booker', 'provider', 'admin'] 103 | }, 104 | providerName: { 105 | type: String, 106 | optional: true 107 | } 108 | }); 109 | 110 | Meteor.users.attachSchema(Schema.User); 111 | -------------------------------------------------------------------------------- /client/lib/routechanging.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Mitchell on 2014-10-24. 3 | */ 4 | changeParams = function(inputObj) { 5 | // Call it like this: changeParams({time: "12:00 AM", date: "2015-02-02", providerName: "Provider"}) 6 | //All input must be ready-for-url 7 | //Provides non-destructive in-place changing of URL parameters 8 | var newparams = {}; 9 | for (param in Router.current().params) { //first, copy out all old params 10 | if (!Router.current().params.hasOwnProperty(param)) { 11 | continue;//ignores BS properties 12 | } 13 | //console.log(Router.current().params[param]); 14 | newparams[param] = Router.current().params[param]; 15 | } 16 | for (param in inputObj) {//then replace/add all input params 17 | if (!Router.current().params.hasOwnProperty(param)) { 18 | continue;//ignores BS properties 19 | } 20 | //console.log(inputObj[param]); 21 | newparams[param] = inputObj[param]; 22 | } 23 | if (inputObj.hasOwnProperty('route')) {//finally, change route if requested. 24 | Router.go(inputObj.route, newparams); 25 | } else { 26 | Router.go(Router.current().route.getName(), newparams); 27 | } 28 | 29 | }; 30 | newAppointment = function(newtime, block) { 31 | //if block is true, we'll go to newBlockout instead 32 | if (typeof block === "undefined") { 33 | block = false; 34 | } 35 | console.log("newAppointment with time " + newtime); 36 | var newparams = {}; 37 | if (newtime && newtime instanceof Date) { 38 | newtime = moment(newtime).format('h-mm-A'); 39 | } else if (newtime && typeof newtime === "string") { 40 | newtime = newtime.replace(':', "-").replace(' ', "-"); 41 | //change 12:40 PM to 12-40-PM 42 | } 43 | if (Router.current().params.providerName && Router.current().params.date) { 44 | newparams.date = Router.current().params.date; 45 | newparams.providerName = Router.current().params.providerName; 46 | } else if (Session.get('date') && Session.get("selectedProviderName")) { 47 | newparams.date = moment(Session.get('date')).startOf('day').format('YYYY-MM-DD'); 48 | newparams.providerName = Session.get("selectedProviderName"); 49 | } else { 50 | console.error("newAppointment called without date or providerName set"); 51 | newparams.date = moment().startOf('day').format('YYYY-MM-DD'); 52 | newparams.providerName = providers.findOne().name; 53 | } 54 | if (newtime) { 55 | newparams.time = newtime; 56 | } else { 57 | newparams.time = "12-00-AM" 58 | } 59 | console.log(newparams); 60 | if (!block) { 61 | Router.go('newAppointment', newparams); 62 | } else { 63 | Router.go('newBlockout', newparams); 64 | } 65 | }; 66 | goHome = function(newDate, newProv) {//newDate is a date obj please 67 | var newparams = {}; 68 | if (Session.get('date') && Session.get('selectedProviderName')) { 69 | newparams.date = moment(Session.get('date')).format('YYYY-MM-DD'); 70 | newparams.providerName= Session.get('selectedProviderName'); 71 | } else { 72 | Router.go('/'); //when moving from non-bookingtable pages. 73 | } 74 | if(newDate) { 75 | newparams.date = moment(newDate).format('YYYY-MM-DD'); 76 | } 77 | if(newProv) { 78 | newparams.providerName = newProv; 79 | } 80 | 81 | Router.go('bookingTable', newparams); 82 | }; -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.2 2 | accounts-password@1.1.4 3 | alanning:roles@1.2.14 4 | aldeed:autoform@5.8.1 5 | aldeed:collection2@2.7.0 6 | aldeed:simple-schema@1.5.1 7 | autoupdate@1.2.4 8 | babel-compiler@5.8.24_1 9 | babel-runtime@0.1.4 10 | base64@1.0.4 11 | binary-heap@1.0.4 12 | blaze@2.1.3 13 | blaze-html-templates@1.0.1 14 | blaze-tools@1.0.4 15 | boilerplate-generator@1.0.4 16 | caching-compiler@1.0.0 17 | caching-html-compiler@1.0.2 18 | callback-hook@1.0.4 19 | check@1.1.0 20 | coffeescript@1.0.11 21 | dburles:mongo-collection-instances@0.3.4 22 | ddp@1.2.2 23 | ddp-client@1.2.1 24 | ddp-common@1.2.2 25 | ddp-rate-limiter@1.0.0 26 | ddp-server@1.2.2 27 | deps@1.0.9 28 | diff-sequence@1.0.1 29 | ecmascript@0.1.6 30 | ecmascript-runtime@0.2.6 31 | ejson@1.0.7 32 | email@1.0.8 33 | fastclick@1.0.7 34 | geojson-utils@1.0.4 35 | gwendall:body-events@0.1.6 36 | hot-code-push@1.0.0 37 | html-tools@1.0.5 38 | htmljs@1.0.5 39 | http@1.1.1 40 | id-map@1.0.4 41 | iron:controller@1.0.12 42 | iron:core@1.0.11 43 | iron:dynamic-template@1.0.12 44 | iron:layout@1.0.12 45 | iron:location@1.0.11 46 | iron:middleware-stack@1.0.11 47 | iron:router@1.0.12 48 | iron:url@1.0.11 49 | jquery@1.11.4 50 | lai:collection-extensions@0.1.4 51 | launch-screen@1.0.4 52 | less@2.5.1 53 | livedata@1.0.15 54 | localstorage@1.0.5 55 | logging@1.0.8 56 | mdg:validation-error@0.1.0 57 | meteor@1.1.10 58 | meteor-base@1.0.1 59 | meteorhacks:kadira@2.26.3 60 | meteorhacks:meteorx@1.4.1 61 | meteorhacks:subs-manager@1.6.3 62 | meteortoys:allthings@2.3.1 63 | meteortoys:authenticate@2.1.0 64 | meteortoys:autopub@2.1.0 65 | meteortoys:blueprint@2.1.0 66 | meteortoys:email@2.1.0 67 | meteortoys:hotreload@2.1.0 68 | meteortoys:listen@2.1.0 69 | meteortoys:method@3.0.2 70 | meteortoys:pub@3.0.2 71 | meteortoys:result@2.1.0 72 | meteortoys:shell@2.1.0 73 | meteortoys:status@2.1.0 74 | meteortoys:sub@2.1.0 75 | meteortoys:throttle@2.1.0 76 | meteortoys:toykit@2.2.1 77 | minifiers@1.1.7 78 | minimongo@1.0.10 79 | mobile-experience@1.0.1 80 | mobile-status-bar@1.0.6 81 | momentjs:moment@2.10.6 82 | momentjs:twix@0.8.1 83 | mongo@1.1.3 84 | mongo-id@1.0.1 85 | mongo-livedata@1.0.9 86 | mrt:moment@2.8.1 87 | mrt:moment-timezone@0.2.1 88 | msavin:jetsetter@1.5.2 89 | msavin:mongol@1.6.2 90 | multiply:iron-router-progress@1.0.2 91 | natestrauser:jquery-scrollto@1.4.7 92 | npm-bcrypt@0.7.8_2 93 | npm-mongo@1.4.39_1 94 | observe-sequence@1.0.7 95 | ordered-dict@1.0.4 96 | promise@0.5.1 97 | raix:eventemitter@0.1.3 98 | random@1.0.5 99 | rate-limit@1.0.0 100 | reactive-dict@1.1.3 101 | reactive-var@1.0.6 102 | reload@1.1.4 103 | retry@1.0.4 104 | routepolicy@1.0.6 105 | rzymek:fullcalendar@2.5.0 106 | service-configuration@1.0.5 107 | session@1.1.1 108 | sha@1.0.4 109 | softwarerero:accounts-t9n@1.1.7 110 | spacebars@1.0.7 111 | spacebars-compiler@1.0.7 112 | srp@1.0.4 113 | standard-minifiers@1.0.2 114 | templating@1.1.5 115 | templating-tools@1.0.0 116 | tracker@1.0.9 117 | tsega:bootstrap3-datetimepicker@4.17.37_1 118 | twbs:bootstrap@3.3.6 119 | ui@1.0.8 120 | underscore@1.0.4 121 | url@1.0.5 122 | useraccounts:bootstrap@1.13.1 123 | useraccounts:core@1.13.1 124 | webapp@1.2.3 125 | webapp-hashing@1.0.5 126 | -------------------------------------------------------------------------------- /server/publications.js: -------------------------------------------------------------------------------- 1 | Meteor.publish("appointmentList", function (date, providerName) { 2 | if(!this.userId) { 3 | this.stop(); 4 | return; 5 | } 6 | console.log(date); 7 | var startDate = moment(date).startOf('day').toDate(); 8 | var endDate = moment(date).endOf('day').toDate(); 9 | try{ 10 | console.log("appointmentList subscribed by " + providers.findOne({name: providerName}).name + " for date "+ date); 11 | //console.log("query is "+JSON.stringify({"date": {$gte: startDate, $lt: endDate}, "providerName": providerName})) 12 | } catch(e) { 13 | console.error("!!!! appointmentList subscribed without providerName!"); 14 | //this.stop(); 15 | //return; 16 | } 17 | return appointmentList.find({"date": {$gte: startDate, $lt: endDate}, "providerName": providerName}); 18 | }); 19 | Meteor.publish(null, function() { 20 | if(!this.userId) { 21 | this.stop(); 22 | return; 23 | } 24 | if (Roles.userIsInRole(this.userId, 'provider')) { 25 | // console.log("providerSubscription subscribed by provider"); 26 | return providers.find({name: Meteor.users.findOne(this.userId).providerName}); 27 | } 28 | //console.log("providerSubscription subscribed by non-provider"); 29 | return providers.find(); 30 | }); 31 | Meteor.publish("unusualDays", function(thedate) { 32 | if(!this.userId) { 33 | this.stop(); 34 | return; 35 | } 36 | return unusualDays.find({date:thedate}) 37 | }); 38 | Meteor.publish("unusualDaysRange", function(dateRangeStart, dateRangeEnd) { 39 | if(!this.userId) { 40 | this.stop(); 41 | return; 42 | } 43 | return unusualDays.find({date: {$gte: dateRangeStart, $lt: dateRangeEnd}}) 44 | }); 45 | Meteor.publish(null, function (){ 46 | if(!this.userId) { 47 | this.stop(); 48 | return; 49 | } 50 | return Meteor.roles.find({});//publish all roles without sub 51 | }); 52 | Meteor.publish(null, function(){ 53 | if(!this.userId) { 54 | this.stop(); 55 | } else if (Roles.userIsInRole(this.userId, "provider")) { 56 | return Meteor.users.find(this.userId, {fields: {providerName: 1}}); 57 | } 58 | });//DOES THIS DO ANYTHING WTF 59 | Meteor.publish("userList", function() { 60 | console.log("userlist caller is admin? " + Roles.userIsInRole(this.userId, 'admin')); 61 | if(!this.userId || !Roles.userIsInRole(this.userId, 'admin')) { 62 | this.stop(); 63 | return; 64 | } 65 | return Meteor.users.find(); 66 | }); 67 | Meteor.publish("blockouts", function(date, provider) { 68 | try { 69 | // console.log("blockouts subscribed for: " + provider); 70 | } 71 | catch(e) { 72 | console.error("blockouts subscribed without provider name"); 73 | //this.stop(); 74 | //return; 75 | } 76 | if(!this.userId) { 77 | this.stop(); 78 | return; 79 | } 80 | var startDate = moment(date).startOf('day').toDate(); 81 | var endDate = moment(date).endOf('day').toDate(); 82 | //console.log({date: {$gte: startDate, $lt: endDate}, providerName: provider}); 83 | //console.log(blockouts.find().fetch()); 84 | return blockouts.find({date: {$gte: startDate, $lt: endDate}, providerName: provider}); 85 | }); 86 | Meteor.publish("singleAppoint", function(id) { 87 | if(!this.userId) { 88 | this.stop(); 89 | return; 90 | } 91 | console.log('singleAppoint subbed: '+id); 92 | return appointmentList.find(id); 93 | }); 94 | Meteor.publish("singleBlockout", function(id) { 95 | if(!this.userId) { 96 | this.stop(); 97 | return; 98 | } 99 | return blockouts.find(id); 100 | }); -------------------------------------------------------------------------------- /lib/providers.js: -------------------------------------------------------------------------------- 1 | providers = new Meteor.Collection("providers"); 2 | // blockoutSchema = new SimpleSchema({ 3 | 4 | // }); 5 | // providers.attachSchema(blockoutSchema); 6 | providers.attachSchema(new SimpleSchema({ 7 | name: { 8 | type: String, 9 | label: "Name", 10 | unique: true, 11 | index: true 12 | }, 13 | startTime: { 14 | type: Number, 15 | label: "Usual Start Hour", 16 | min: 0, 17 | max: 22, 18 | //TODO: When this is set, verify that no 19 | //current appointments voilate the new boundries 20 | defaultValue: 9, 21 | custom: function(){ 22 | if (this.field("endTime").value <= this.value){ 23 | return "before" 24 | } 25 | if (Meteor.isServer) { 26 | var appoints = appointmentList.find({providerName: this.field('name').value}).fetch(); 27 | //console.log(appoints); 28 | for (var appointIndex in appoints) { 29 | if (appoints.hasOwnProperty(appointIndex)) { 30 | console.log("comparing " + moment(appoints[appointIndex].date).hours() + " with new hour: "+this.value); 31 | if (moment(appoints[appointIndex].date).hours() < this.value) { 32 | return "wouldPushOutOfBounds"; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | endTime: { 40 | type: Number, 41 | label: "Usual End Hour (24h)", 42 | min: 1, 43 | max: 23, 44 | defaultValue: 17, 45 | custom: function(){ 46 | //need to get server to iterate over all appointments 47 | //and check they are still valid in new bounds 48 | if (Meteor.isServer) { 49 | var appoints = appointmentList.find({providerName: this.field('name').value}).fetch(); 50 | for (var appointIndex in appoints) { 51 | if (appoints.hasOwnProperty(appointIndex)) { 52 | if (moment(appoints[appointIndex].date).hours() > this.value) { 53 | return "wouldPushOutOfBounds"; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | appointmentLength: { 61 | type: Number, 62 | label: "Usual Appointment Length", 63 | min: 5, 64 | max: 120, 65 | defaultValue: 15, 66 | custom: function(){ 67 | if (this.value % 5 !== 0){ 68 | return "mod5"; 69 | } 70 | } 71 | }, 72 | blockouts: { 73 | type: [Object], 74 | // maxCount: 7, 75 | optional: true 76 | }, 77 | "blockouts.$.day": { 78 | type: String, 79 | label: "Day of week", 80 | allowedValues: [ 81 | "monday", 82 | "tuesday", 83 | "wednesday", 84 | "thursday", 85 | "friday", 86 | "saturday", 87 | "sunday", 88 | "all" ] 89 | }, 90 | "blockouts.$.title": { 91 | type: String, 92 | label: "Title" 93 | }, 94 | "blockouts.$.time": { 95 | type: String, 96 | label: "Start Time", 97 | regEx: /^[0-2]?\d:\d\d [APap]m|M$/, 98 | autoform: { 99 | type: "timePicker" 100 | }, 101 | custom: function() { 102 | var dateObj = moment(this.value, "h:mm A"); 103 | var compareTwix = moment(dateObj).twix(moment(dateObj).add(this.siblingField('length').value, "minutes")); 104 | var provObject = providers.findOne(this.docId); 105 | var exampleTwix = moment().startOf('day').hour(provObject.startTime).twix( 106 | moment().startOf('day').hour(provObject.endTime)); 107 | if (!exampleTwix.engulfs(compareTwix)) { 108 | return "dateOutOfBounds" 109 | } 110 | } 111 | }, 112 | "blockouts.$.length": { 113 | type: Number, 114 | label: "Length", 115 | min:5, 116 | defaultValue: 15, 117 | autoform: { 118 | step: 5 119 | }, 120 | custom: function(){ 121 | if (this.value % 5 !== 0){ 122 | return "mod5"; 123 | } 124 | } 125 | } 126 | })); 127 | 128 | // blockoutSchema.simpleSchema().messages({ 129 | // mod5: "[label] must be a multiple of 5", 130 | // }) 131 | providers.simpleSchema().messages({ 132 | mod5: "[label] must be a multiple of 5", 133 | before: "[label] must be before End Time.", 134 | wouldPushOutOfBounds: "This change would push appointments out of bounds.", 135 | dateOutOfBounds: "This repeating blockout is out of the usual day for this provider." 136 | }); 137 | if (Meteor.isServer){ 138 | if (providers.find({}).fetch().length === 0) { 139 | providers.insert({name: "Default Provider"}) 140 | } 141 | } 142 | 143 | // console.log(providers.find().fetch()) 144 | -------------------------------------------------------------------------------- /client/views/appointments/tableItemHelpers.js: -------------------------------------------------------------------------------- 1 | jquerycache = {};//cache the jquery calls because they're slow. do em once per route, clearing on every new route. 2 | fillJqueryCache = function() { 3 | Session.get("date"); 4 | Session.get('selectedProviderName'); 5 | //console.log('filling the jquery cache'); 6 | jquerycache.theadth = $("thead th").css("height");//table header height 7 | jquerycache.rowHeight = $(".timeRow")[1].clientHeight;//the first row is different height between browsers. 8 | jquerycache.headerWidth = parseInt($(".rowHeader").css("width")); 9 | jquerycache.tableItemHeight = parseInt($('.tableItemData').css('height')); 10 | // see https://github.com/twbs/bootstrap/issues/16149 11 | 12 | }; 13 | 14 | buildTableItemStyle = function(thisobj) {//centralizing this function improves DRY and 15 | //allows us to avoid doing any of this expensive stuff until the times table is rendered 16 | if (!Session.get('timesRendered')) { 17 | return 0; 18 | } 19 | var height = tableItemHeight(thisobj); 20 | return "style=\"" + 21 | "width:auto;max-height:" +height+ 22 | ";height:" + height + 23 | ";left:" + tableItemLeft(thisobj) + 24 | ";top:" + tableItemTop(thisobj) + 25 | ";\"" 26 | }; 27 | 28 | highlightItemHelper = function(thisobj) { 29 | if(typeof Session.get('currentlyEditingDoc') !== "undefined" 30 | && Session.get("currentlyEditingDoc") === thisobj._id) { 31 | 32 | return "being-edited"; 33 | } 34 | }; 35 | 36 | inBetween = function(thisobj) { 37 | if (!Session.get('timesRendered')) { 38 | return 0; 39 | } 40 | var provObj = getProvObject(Session.get("date"), Session.get('selectedProviderName')); 41 | if (((jquerycache.rowHeight/provObj.appointmentLength)*thisobj.length) >= (jquerycache.tableItemHeight * 4)) { 42 | return '
' 43 | } 44 | else { 45 | return " - " 46 | } 47 | }; 48 | 49 | tableItemHeight = function(thisobj) { 50 | if (thisobj.length == Session.get("appntlength")) 51 | { 52 | return jquerycache.rowHeight +"px"; 53 | } 54 | var provObject = getProvObject(Session.get("date"), Session.get('selectedProviderName')); 55 | var defaultHeight = jquerycache.rowHeight; 56 | var pxPerMinute = defaultHeight/provObject.appointmentLength; 57 | return Math.ceil(pxPerMinute * thisobj.length) + "px"; 58 | }; 59 | tableItemLeft = function(thisobj) { 60 | return jquerycache.headerWidth + "px"; 61 | }; 62 | tableItemTop = function(thisobj) { 63 | if (!thisobj.date) {//this is a blockout 64 | var datestring = moment(Session.get("date")).tz("Pacific/Auckland").format("YYYY-MM-DD ") + thisobj.time; 65 | var thedate = moment(datestring, "YYYY-MM-DD hh:mm A").toDate(); 66 | // console.log(thedate); 67 | } else { 68 | thedate = thisobj.date 69 | } 70 | var provObject = getProvObject(Session.get("date"), Session.get('selectedProviderName')); 71 | 72 | var startTime = Session.get("date"); 73 | startTime.setHours(provObject.startTime); 74 | //numFromTop is the number of minutes from the beginning of the day this item is 75 | var numFromTop = (thedate.getTime() - 76 | startTime.getTime())/1000/60; 77 | 78 | 79 | if(numFromTop/provObject.appointmentLength === 0){//special case for first item of the day. 80 | return jquerycache.theadth; 81 | } 82 | else 83 | { 84 | var untouchedAppntsFromTop = (numFromTop/provObject.appointmentLength)+1; 85 | 86 | var appntsFromTop = Math.floor(untouchedAppntsFromTop); 87 | var pixelsFromTop = jquerycache.rowHeight*appntsFromTop; 88 | 89 | if (untouchedAppntsFromTop % 1 !== 0){ 90 | // //if the appnt doesn't align with standard boundries - i.e, 15 mins 91 | pixelsFromTop += jquerycache.rowHeight * 92 | (untouchedAppntsFromTop % 1); 93 | } 94 | //console.log("Editing: "+Session.get("currentlyEditingDoc")) 95 | if(Session.equals("currentlyEditingDoc", thisobj._id) && thisobj._id)/*if _id is null we are a blockout*/ { 96 | 97 | Session.set("scrollToPoint", pixelsFromTop); 98 | } else if (Session.equals("changedAppointmentID", thisobj._id) 99 | && typeof Session.get("changedAppointmentID") !== "undefined") { 100 | Session.set("scrollToPoint", pixelsFromTop); 101 | Session.set("changedAppointmentID", null); 102 | } 103 | return pixelsFromTop + "px"; 104 | } 105 | }; -------------------------------------------------------------------------------- /lib/unusualdays.js: -------------------------------------------------------------------------------- 1 | checkBounds = function(thisobj) { 2 | //used for checking whether the new bounds of an unusual day are valid. Used on creation and on edit of hours 3 | //validity is determined by whether any appointments are currently out of bounds. 4 | if (Meteor.isServer) { 5 | try {//existing 6 | var cleanDate = moment(unusualDays.findOne(thisobj.docId).date); 7 | var provider = unusualDays.findOne(thisobj.docId).providerName; 8 | } catch (e) {//new 9 | cleanDate = moment(thisobj.field("date").value); 10 | provider = thisobj.field("providerName").value; 11 | } 12 | } else {//client 13 | try { 14 | cleanDate = moment(unusualDays.findOne({date: Session.get('date')}).date); 15 | } catch (e) { //new unusual day. 16 | cleanDate = moment(Session.get('date')); 17 | } 18 | provider = Session.get("selectedProviderName"); 19 | } 20 | //new start and end of working day 21 | var providerObj = providers.findOne({name: provider}); 22 | if(thisobj.key ==="startTime") { 23 | var startDate = cleanDate.clone().tz("Pacific/Auckland").hour(thisobj.value).toDate(); 24 | var endDate = cleanDate. 25 | clone(). 26 | tz("Pacific/Auckland"). 27 | hour(thisobj.field("endTime").value). 28 | add(providerObj.appointmentLength, "minutes"). 29 | toDate(); 30 | } else { 31 | startDate = cleanDate.clone().tz("Pacific/Auckland").hour(thisobj.field("startTime").value).toDate(); 32 | endDate = cleanDate.clone().tz("Pacific/Auckland").hour(thisobj.value).add(providerObj.appointmentLength, "minutes").toDate(); 33 | } 34 | 35 | var dayTwix = moment(startDate).twix(endDate); 36 | 37 | //start and end of day for query 38 | //console.log(cleanDate.format()); 39 | 40 | var midnight = moment(cleanDate).startOf("day").toDate(); 41 | var midday = moment(cleanDate).endOf("day").toDate();//it's not actually midday but fuck the police 42 | var appoints = appointmentList.find({date: {$gte: midnight, $lt: midday}, 43 | providerName: provider}).fetch(); 44 | var theblockouts = getBlockouts(); 45 | var ret = null; 46 | _.each(_.union(appoints,theblockouts), function(appoint) {//check each appointment and blockout still fits 47 | console.log("comparing "+appoint.time+" to "+dayTwix.format()); 48 | //console.log(appoint); 49 | if (appoint.hasOwnProperty("date")) { 50 | if(!dayTwix.engulfs(moment(appoint.date).twix(moment(appoint.date).add(appoint.length, 'minutes')))) { 51 | console.log("fail"); 52 | ret = "dateOutOfBounds"; 53 | } 54 | } else { //this is a blockout from the provider 55 | var blockStartDate = moment(cleanDate.tz('Pacific/Auckland').format('YYYY MM DD ') + appoint.time, 56 | "YYYY MM DD HH:mm A"); 57 | var blockTwix = moment(blockStartDate).twix(moment(blockStartDate).add(appoint.length, "minutes")); 58 | if (!(dayTwix.engulfs(blockTwix))) { 59 | console.log("fail"); 60 | ret = "dateOutOfBounds"; 61 | } 62 | } 63 | 64 | }); 65 | return ret; 66 | }; 67 | unusualDays = new Meteor.Collection("unusualDays"); 68 | unusualDays.attachSchema( new SimpleSchema({ 69 | date: { 70 | type: Date, 71 | label: "Date", 72 | index: 1, 73 | autoValue: function(isInsert) { 74 | if(!isInsert) { 75 | this.unset(); 76 | } 77 | } 78 | }, 79 | providerName: { 80 | type: String, 81 | label: "Provider Name" 82 | // custom: 83 | //TODO: Make sure provider Name exists 84 | }, 85 | notes: { 86 | type: String, 87 | label: "Notes", 88 | optional: true 89 | }, 90 | startTime: { 91 | type: Number, 92 | label: "Start Time", 93 | min:1, 94 | max:23, 95 | custom: function() { 96 | if (this.isSet) { 97 | return checkBounds(this); 98 | } 99 | } 100 | }, 101 | endTime: { 102 | type: Number, 103 | label: "End Time (24h time)", 104 | autoValue: function() { 105 | if(!this.isSet && this.isInsert && this.field("providerName").isSet) { 106 | return providers.findOne({name: this.field("providerName").value})["endTime"]; 107 | } 108 | }, 109 | min:2, 110 | max:24, 111 | custom: function(){ 112 | if (this.isSet) { 113 | if (this.field("startTime").value >= this.value){ 114 | return "startAfterEnd" 115 | } 116 | return checkBounds(this); 117 | } 118 | 119 | } 120 | }, 121 | appointmentLength: { 122 | type: Number, 123 | label: "Appointment Length", 124 | min: 1, 125 | max: 60, 126 | custom: function(){ 127 | if (this.value % 5 !== 0){ 128 | return "mod5"; 129 | } 130 | }, 131 | autoValue: function() { 132 | if(this.isInsert && this.field("providerName").isSet) { 133 | return providers.findOne({name: this.field("providerName").value})["appointmentLength"]; 134 | } 135 | } 136 | } 137 | })); 138 | unusualDays.simpleSchema().messages({ 139 | mod5: "[value] must be a multiple of 5.", 140 | startAfterEnd:"Start Time must be before End Time.", 141 | endBeforeStart:"End time must be after Before time.", 142 | dateOutOfBounds: "An Appointment or Blockout would be put out of bounds by this change." 143 | }); 144 | -------------------------------------------------------------------------------- /client/views/blockouts/addBlockout.js: -------------------------------------------------------------------------------- 1 | 2 | function dayDelta(date) { 3 | var diff = moment(date).diff(moment().startOf('day'), "days"); 4 | if (diff===1){ 5 | return " tomorrow"; 6 | } 7 | else if (diff===-1) { 8 | return " yesterday"; 9 | } 10 | else if (diff === 0) 11 | { 12 | return " today" 13 | } 14 | else if (diff > 1) 15 | { 16 | return " in " +Math.abs(diff)+ " days" 17 | } 18 | else 19 | { 20 | return " "+Math.abs(diff)+" days ago" 21 | } 22 | } 23 | Template.insertBlockoutForm.events({ 24 | 'click #closeBlockoutEditor': function() { 25 | $('td.rowContent.bg-success').removeClass('bg-success'); 26 | goHome(); 27 | }, 28 | 'click #deleteBlockoutButton': function() { 29 | if (confirm("Are you sure you want to delete this blockout?")) { 30 | goHome(); 31 | blockouts.remove(Session.get("currentlyEditingDoc")); 32 | } 33 | } 34 | 35 | }); 36 | Template.insertBlockoutForm.rendered = function() { 37 | $('#datetimepicker').on("dp.change", function (e) { 38 | //if (Session.equals("newTime", moment(date).format("h:mm A"))) {return;}//prevent loops 39 | if (Router.current().route.getName() === "newBlockout") { 40 | newAppointment(e.date.format("h:mm A"), true); 41 | } 42 | }); 43 | }; 44 | Template.insertBlockoutForm.helpers({ 45 | title: function(){ 46 | if (Session.get("formForInsert")) { 47 | return "Add New Blockout" 48 | } else { 49 | return "Editing Blockout"; 50 | } 51 | 52 | }, 53 | subtitle: function() { 54 | if (Session.get("formForInsert")) { 55 | var momentobj = moment(Session.get("date")); 56 | var ret = momentobj.format("dddd, MMMM Do GGGG"); 57 | return "New Blockout for " + ret + " -"+ dayDelta(Session.get("date")); 58 | } else {} 59 | }, 60 | savebuttontext: function() { 61 | if (Session.get("formForInsert")) { 62 | return "Create New Blockout" 63 | } else { 64 | return "Update Blockout" 65 | } 66 | }, 67 | sessionDate: function(){return Session.get("date")}, 68 | length: function() { 69 | Session.get("newTime"); 70 | if (Session.get("formForInsert")) { 71 | var provObject = getProvObject(Session.get("date"), Session.get('selectedProviderName')); 72 | try {return provObject.appointmentLength} 73 | catch (e) { 74 | return 0; 75 | }//this error doesn't matter, it means the unusualDays 76 | // and Providers collections aren't filled yet. 77 | //will be fixed for real when iron router is used for appointment editing 78 | ///creation 79 | } else {//update, grab length from current doc 80 | return blockouts.findOne(Session.get("currentlyEditingDoc")).length; 81 | } 82 | }, 83 | currentType: function() { 84 | if(Session.get("formForInsert")) { 85 | return "insert" 86 | } 87 | else { 88 | return "update" 89 | } 90 | }, 91 | timePreset: function() {//FIXME 92 | if (Session.get("formForInsert")) { 93 | if (!(typeof Session.get("newTime") === "undefined")) { 94 | return Session.get("newTime"); 95 | } else { 96 | return "12:00 PM"; 97 | } 98 | } else { 99 | return blockouts.findOne(Session.get("currentlyEditingDoc")).time; 100 | } 101 | }, 102 | currentDoc: function() {return blockouts.findOne(Session.get("currentlyEditingDoc"));}, 103 | deleteButtonClass: function() {if (Session.get("formForInsert")) { 104 | return "hidden"; 105 | }} 106 | 107 | }); 108 | AutoForm.hooks({ 109 | insertBlockoutFormInner: { 110 | beginSubmit: function(fieldId, template) { 111 | var succAlert = $('#insertSuccessAlert'); 112 | succAlert[0].innerHTML = "Submitting..."; 113 | succAlert.show("fast");//possible race condition? If error occours and form is correctly submitted within 3000ms 114 | $('#saveAppointChanges').attr("disabled", true); 115 | }, 116 | formToDoc: function(doc){ 117 | doc.providerName = Session.get("selectedProviderName"); 118 | return doc; 119 | }, 120 | onSuccess: function(operation, result) { 121 | if(operation === "update") { 122 | $('#insertSuccessAlert')[0].innerHTML = "Blockout Successfully Edited."; 123 | } else { 124 | $('#insertSuccessAlert')[0].innerHTML = "New Blockout Created."; 125 | } 126 | var succAlert = $('#insertSuccessAlert'); 127 | succAlert.removeClass('alert-danger alert-info alert-info alert-success'); 128 | succAlert.addClass('alert-success'); 129 | $('td.rowContent.bg-success').removeClass('bg-success'); 130 | //closeTimeout = Meteor.setTimeout(function() { 131 | goHome(); 132 | //}, 3000); 133 | }, 134 | formToModifier: function(doc) { 135 | doc.$set.providerName = Session.get("selectedProviderName"); 136 | return doc; 137 | }, 138 | onError: function(operation, error) { 139 | $('#saveAppointChanges').attr("disabled", false); 140 | var alert = $('#insertSuccessAlert'); 141 | alert.removeClass('alert-danger alert-info alert-info alert-success'); 142 | alert.addClass('alert-danger'); 143 | alert[0].innerHTML = "Uh-oh, something went wrong!"; 144 | alert.show("fast"); 145 | Meteor.setTimeout(function() { 146 | $('#insertSuccessAlert').hide("slow"); 147 | }, 3000); 148 | 149 | }, 150 | after: { 151 | insert: function(error, result) { 152 | if (error) { 153 | console.log("Insert Error:", error); 154 | $("#insertSuccessAlert").alert(); 155 | } else { 156 | console.log("Insert Result:", result); 157 | } 158 | } 159 | } 160 | } 161 | }); 162 | -------------------------------------------------------------------------------- /server/dbrules.js: -------------------------------------------------------------------------------- 1 | unusualDays.deny({ 2 | remove: function(userId, doc) { 3 | //ensure that no appointments will be left stranded by this day being removed. 4 | var provider = doc.providerName; 5 | var cleanDate = moment(doc.date); 6 | var provObj = providers.findOne({name: provider}); 7 | var startDate = cleanDate.clone().tz("Pacific/Auckland").hour(provObj.startTime).toDate(); 8 | var endDate = cleanDate.clone().tz("Pacific/Auckland").hour(provObj.endTime).toDate(); 9 | var dayTwix = moment(startDate).twix(endDate); 10 | console.log(dayTwix.format()); 11 | //build query 12 | var midnight = moment(cleanDate).startOf("day").toDate(); 13 | var midday = moment(cleanDate).endOf("day").toDate(); 14 | var appoints = appointmentList.find({date: {$gte: midnight, $lt: midday}, 15 | providerName: provider}).fetch(); 16 | var ret = false; 17 | _.each(appoints, function(appoint) { 18 | console.log(appoint); 19 | if(!dayTwix.contains(appoint.date)) { 20 | console.log("fail"); 21 | ret = true; 22 | } 23 | }); 24 | return ret; 25 | } 26 | }); 27 | 28 | appointmentList.allow({ 29 | insert: function(userId, appointment) { 30 | if (Roles.userIsInRole(userId, 'provider') && appointment.providerName != Meteor.users.findOne(userId).providerName) { 31 | throw new Meteor.Error (403, "Provider tried to add appointment for user other than herself.") 32 | } 33 | return true; 34 | }, 35 | update: function(userId, appointment) { 36 | 37 | if (Roles.userIsInRole(userId, 'provider') && appointment.providerName != Meteor.users.findOne(userId).providerName) { 38 | throw new Meteor.Error (403, "Provider tried to edit appointment for user other than herself."); 39 | } 40 | return true; 41 | }, 42 | remove:function(userId, appointment) { 43 | 44 | if (Roles.userIsInRole(userId, 'provider') && appointment.providerName != Meteor.users.findOne(userId).providerName) { 45 | throw new Meteor.Error (403, "Provider tried to delete appointment for user other than herself."); 46 | } 47 | return true; 48 | }, 49 | fetch: ["providerName"] 50 | }); 51 | 52 | blockouts.allow({ 53 | insert: function(userId, blockout) { 54 | if (Roles.userIsInRole(userId, 'provider') && blockout.providerName != Meteor.users.findOne(userId).providerName) { 55 | throw new Meteor.Error (403, "Provider tried to add blockout for user other than herself.") 56 | } 57 | return true; 58 | }, 59 | update: function(userId, blockout) { 60 | 61 | if (Roles.userIsInRole(userId, 'provider') && blockout.providerName != Meteor.users.findOne(userId).providerName) { 62 | throw new Meteor.Error (403, "Provider tried to edit blockout for user other than herself."); 63 | } 64 | return true; 65 | }, 66 | remove:function(userId, blockout) { 67 | 68 | if (Roles.userIsInRole(userId, 'provider') && blockout.providerName != Meteor.users.findOne(userId).providerName) { 69 | throw new Meteor.Error (403, "Provider tried to delete blockout for user other than herself."); 70 | } 71 | return true; 72 | }, 73 | fetch: ["providerName"] 74 | }); 75 | 76 | Meteor.users.allow({ 77 | update: function(userId, user) { 78 | if(Roles.userIsInRole(userId, 'admin')) { 79 | return true; 80 | } 81 | throw new Meteor.Error(403, "Nice try punk. Only admins can edit users.") 82 | }, 83 | insert: function(userId, user) { 84 | if(Roles.userIsInRole(userId, 'admin')) { 85 | return true; 86 | } 87 | throw new Meteor.Error(403, "Nice try punk. Only admins can add users.") 88 | }, 89 | remove: function(userId, user) { 90 | if(Roles.userIsInRole(userId, 'admin')) { 91 | return true; 92 | } 93 | throw new Meteor.Error(403, "Nice try punk. Only admins can delete users.") 94 | } 95 | }); 96 | 97 | unusualDays.allow({ 98 | insert: function(userId, unusualDay) { 99 | if (Roles.userIsInRole(userId, 'provider') && unusualDay.providerName != Meteor.users.findOne(userId).providerName) { 100 | throw new Meteor.Error (403, "Provider tried to add unusualDay for user other than herself.") 101 | } 102 | return true; 103 | }, 104 | update: function(userId, unusualDay) { 105 | 106 | if (Roles.userIsInRole(userId, 'provider') && unusualDay.providerName != Meteor.users.findOne(userId).providerName) { 107 | throw new Meteor.Error (403, "Provider tried to edit blockout for user other than herself."); 108 | } 109 | return true; 110 | }, 111 | remove:function(userId, unusualDay) { 112 | 113 | if (Roles.userIsInRole(userId, 'provider') && unusualDay.providerName != Meteor.users.findOne(userId).providerName) { 114 | throw new Meteor.Error (403, "Provider tried to delete blockout for user other than herself."); 115 | } 116 | return true; 117 | }, 118 | fetch: ["providerName"] 119 | }); 120 | 121 | providers.allow({ 122 | insert: function(userId, provider) { 123 | if (Roles.userIsInRole(userId, 'provider') && provider.name != Meteor.users.findOne(userId).providerName) { 124 | throw new Meteor.Error (403, "Provider tried to add provider for user other than herself.") 125 | } 126 | return true; 127 | }, 128 | update: function(userId, provider) { 129 | 130 | if (Roles.userIsInRole(userId, 'provider') && provider.name != Meteor.users.findOne(userId).providerName) { 131 | throw new Meteor.Error (403, "Provider tried to edit provider for user other than herself."); 132 | } 133 | return true; 134 | }, 135 | remove:function(userId, provider) { 136 | 137 | if (Roles.userIsInRole(userId, 'provider') && provider.name != Meteor.users.findOne(userId).providerName) { 138 | throw new Meteor.Error (403, "Provider tried to delete provider for user other than herself."); 139 | } 140 | return true; 141 | }, 142 | fetch: ["name"] 143 | }); -------------------------------------------------------------------------------- /lib/objects.js: -------------------------------------------------------------------------------- 1 | getProvObject = function(date, providerName) { 2 | var provObject = unusualDays.findOne({date: date, providerName: providerName}); 3 | if (typeof provObject === "undefined") { 4 | provObject = providers.findOne({name: providerName}); 5 | } 6 | return provObject; 7 | }; 8 | 9 | getBlockouts = function(providerName, date) { 10 | //Returns a complete list of blockouts for a given provider and day. 11 | var day = moment(date).format("dddd").toLowerCase(); 12 | if (unusualDays.findOne({date: date})) { 13 | var providerBlockouts = [] 14 | } else { 15 | try { 16 | providerBlockouts = providers.findOne({name: providerName}, {fields: {"blockouts": 1}}).blockouts; 17 | } catch(e) {}//fails if no blockout object on the provider. this is normal. 18 | 19 | } 20 | var singleDayBlockouts = blockouts.find( 21 | {date: 22 | {$gte: moment(date).startOf('day').toDate(), 23 | $lt: moment(date).endOf('day').toDate()}, 24 | providerName: providerName}).fetch(); 25 | 26 | var ret = _.union(providerBlockouts, singleDayBlockouts); 27 | ret = _.filter(ret, function(block) { 28 | if (typeof block !== "undefined" && !block.hasOwnProperty("day")) { 29 | return true;//this means the blockout is custom for today, thus kept 30 | } else if (typeof block !== "undefined") { 31 | return (block.day === day || block.day === "all"); 32 | }//this means the blockout is a recurring one for this provider, so kept if 33 | //is an everyday one or for this day of the week. 34 | return false; 35 | }); 36 | //console.log(ret); 37 | return ret; 38 | }; 39 | 40 | checkDate = function(thisobj, isAppnt) { 41 | //Checks a given date falls within the bounds of the current day and does not overlap 42 | //appointments or blockouts 43 | 44 | ////////////////////////////////////////////////// 45 | /////////////CHECKING DAY BOUNDS 46 | ///////////////////////////////////////////////// 47 | console.log(thisobj.field("providerName").value); 48 | var cleanDate = moment(thisobj.value).startOf('day'); 49 | var provObject = getProvObject(cleanDate.toDate(), thisobj.field("providerName").value); 50 | if (typeof provObject === "undefined" && Meteor.isClient) { 51 | provObject = getProvObject(cleanDate.toDate(), Session.get("selectedProviderName")); 52 | } 53 | console.log("performing checkDate, cleanDate: ", cleanDate.format(), ", thisobj: ", thisobj); 54 | //thisobj may fail due to timezone 55 | console.log("checking "+moment(thisobj.value).format()+ 56 | " is inside "+ moment(cleanDate).tz("Pacific/Auckland").hours(provObject.startTime).format() 57 | + " and "+ moment(cleanDate).tz("Pacific/Auckland").hours(provObject.endTime).format()); 58 | if(moment(thisobj.value).isValid() === false) { 59 | return "wtf" 60 | } 61 | else if (moment(thisobj.value).isBefore(moment(cleanDate).tz("Pacific/Auckland").hours(provObject.startTime).utc())) { 62 | return "dateOutOfBounds" 63 | } 64 | else if (moment(thisobj.value).isAfter(moment(cleanDate).tz("Pacific/Auckland").hours(provObject.endTime).utc())) { 65 | return "dateOutOfBounds" 66 | } 67 | ////////////////////////////////////////////////// 68 | /////////////CHECKING APPOINTMENT OVERLAP 69 | ///////////////////////////////////////////////// 70 | var currentAppoint = thisobj.value; 71 | var currentAppointEnd = moment(currentAppoint).add(parseInt(thisobj.field("length").value), 'minutes'); 72 | var currentRange = moment(currentAppoint).twix(currentAppointEnd); 73 | var queryStart = moment(thisobj.value).startOf('day').toDate(); 74 | var queryEnd = moment(thisobj.value).endOf('day').toDate(); 75 | //console.log(JSON.stringify({date: {$gte: queryStart, $lt:queryEnd},providerName: thisobj.field("providerName").value})); 76 | var appoints = appointmentList.find({date: {$gte: queryStart, $lt:queryEnd},providerName: thisobj.field("providerName").value}).fetch(); 77 | var ret; 78 | _.each(appoints, function(comparedAppoint) { 79 | var comparedRange = moment(comparedAppoint.date) 80 | .twix(moment(comparedAppoint.date) 81 | .add(comparedAppoint.length, "minutes")); 82 | 83 | //console.log("Comparing " + currentRange.format() + " with " + comparedRange.format()); 84 | var overlaps = currentRange.overlaps(comparedRange); 85 | if (overlaps) { 86 | if (Meteor.isServer) { 87 | if (!(thisobj.docId === comparedAppoint._id)) { 88 | console.log("different appointments clashing"); 89 | ret = "overlappingDates"; 90 | return "overlappingDates"; 91 | } 92 | } 93 | else if (Meteor.isClient) { 94 | if (!(Session.get("currentlyEditingDoc") === comparedAppoint._id)) { 95 | console.log("different appointments clashing"); 96 | ret = "overlappingDates"; 97 | return "overlappingDates"; 98 | } 99 | } 100 | } 101 | }); 102 | if (typeof ret === "string") { 103 | return ret; 104 | } 105 | ////////////////////////////////////////////////// 106 | /////////////CHECKING BLOCKOUT OVERLAP 107 | ///////////////////////////////////////////////// 108 | var blockouts = getBlockouts(thisobj.field("providerName").value, 109 | cleanDate.toDate()); 110 | _.each(blockouts, function(comparedBlockout) { 111 | var blockStartDate = moment(cleanDate.tz('Pacific/Auckland').format('YYYY MM DD ') + comparedBlockout.time, 112 | "YYYY MM DD HH:mm A"); 113 | var blockEndDate = moment(blockStartDate).add(comparedBlockout.length, 'minutes'); 114 | var blockTwix = moment(blockStartDate).twix(blockEndDate); 115 | //console.log(); 116 | //console.log("comparing block " + blockTwix.format() + " with appointment "+ currentRange.format()); 117 | var overlaps = blockTwix.overlaps(currentRange); 118 | if (overlaps) { 119 | if (Meteor.isServer) { 120 | //if (!(thisobj.docId === comparedBlockout._id)) { 121 | // console.log("clashing with blockout"); 122 | // ret = "overlappingBlockout"; 123 | // return "overlappingBlockout"; 124 | //} 125 | } 126 | else if (Meteor.isClient) { 127 | 128 | 129 | if (!(Session.get("currentlyEditingDoc") === comparedBlockout._id)) { 130 | if(isAppnt) { 131 | if(confirm('This appointment overlaps a blockout. Are you sure you want to place it here?')) { 132 | return; 133 | } 134 | } 135 | //console.log("clashing with blockout"); 136 | ret = "overlappingBlockout"; 137 | return "overlappingBlockout"; 138 | } 139 | } 140 | } 141 | }); 142 | if (typeof ret === "string") { 143 | return ret; 144 | } 145 | }; 146 | 147 | -------------------------------------------------------------------------------- /client/views/appointments/appointmentEdit.js: -------------------------------------------------------------------------------- 1 | function dayDelta(date) { 2 | var diff = moment(date).diff(moment().startOf('day'), "days"); 3 | if (diff===1){ 4 | return " tomorrow"; 5 | } 6 | else if (diff===-1) { 7 | return " yesterday"; 8 | } 9 | else if (diff === 0) 10 | { 11 | return " today" 12 | } 13 | else if (diff > 1) 14 | { 15 | return " in " +Math.abs(diff)+ " days" 16 | } 17 | else 18 | { 19 | return " "+Math.abs(diff)+" days ago" 20 | } 21 | } 22 | Template.insertAppointmentForm.events({ 23 | 'click #closeBookingEditor': function() { 24 | $('td.rowContent.bg-success').removeClass('bg-success'); 25 | goHome(); 26 | }, 27 | 'click #deleteAppointment': function() { 28 | if (confirm("Are you sure you want to delete this appointment?")) { 29 | appointmentList.remove(Session.get("currentlyEditingDoc")); 30 | goHome(); 31 | } 32 | } 33 | 34 | }); 35 | Template.insertAppointmentForm.rendered = function() { 36 | console.log("appointment edit rendered"); 37 | //$('input[name="date"]').change(function() { 38 | // if (Router.current().route.getName() === "newAppointment" || 39 | // Router.current().route.getName() === "bookingTable") { 40 | // newAppointment($('input[name="date"]').val()); 41 | // } 42 | //}); 43 | $('#datetimepicker').on("dp.change", function (e) { 44 | if (Router.current().route.getName() === "newAppointment" || 45 | Router.current().route.getName() === "bookingTable") { 46 | newAppointment(e.date.format("h:mm A")); 47 | } 48 | }); 49 | }; 50 | Template.insertAppointmentForm.helpers({ 51 | appointmentList: appointmentList, 52 | title: function(){ 53 | if (Session.get("formForInsert")) { 54 | return "Add New Appointment" 55 | } else { 56 | return "Editing Appointment"; 57 | } 58 | 59 | }, 60 | subtitle: function() { 61 | if (Session.get("formForInsert")) { 62 | var momentobj = moment(Session.get("date")); 63 | var ret = momentobj.format("dddd, MMMM Do GGGG"); 64 | return "New Appointment for " + ret + " -"+ dayDelta(Session.get("date")); 65 | } else {} 66 | }, 67 | savebuttontext: function() { 68 | if (Session.get("formForInsert")) { 69 | return "Create New Appointment" 70 | } else { 71 | return "Update Appointment" 72 | } 73 | }, 74 | sessionDate: function(){return Session.get("date")}, 75 | length: function() { 76 | var lol = Session.get("newTime"); 77 | if (Session.get("formForInsert")) { 78 | var provObject = getProvObject(Session.get("date"), Session.get('selectedProviderName')); 79 | try {return provObject.appointmentLength} 80 | catch (e) { 81 | console.log("looking for appointment length too early."); 82 | return 15; 83 | }//this error doesn't matter, it means the unusualDays 84 | // and Providers collections aren't filled yet. 85 | //will be fixed for real when iron router is used for appointment editing 86 | ///creation 87 | } else {//update, grab length from current doc 88 | return appointmentList.findOne(Session.get("currentlyEditingDoc")).length; 89 | } 90 | }, 91 | currentType: function() { 92 | if(Session.get("formForInsert")) { 93 | return "insert" 94 | } 95 | else { 96 | return "update" 97 | } 98 | }, 99 | timePreset: function() { 100 | if (Session.get("formForInsert")) { 101 | if (Session.get('newTime') && typeof Session.get('newTime') !== "undefined") { 102 | return Session.get("newTime"); 103 | } else { 104 | return "12:00 PM"; 105 | } 106 | } else { 107 | return appointmentList.findOne(Session.get("currentlyEditingDoc")).date; 108 | } 109 | }, 110 | currentDoc: function() {return appointmentList.findOne(Session.get("currentlyEditingDoc"))}, 111 | deleteButtonClass: function() {if (Session.get("formForInsert")) { 112 | return "hidden"; 113 | }} 114 | 115 | }); 116 | AutoForm.hooks({ 117 | insertAppointmentFormInner: { 118 | beginSubmit: function(fieldId, template) { 119 | var thealert = $('#insertSuccessAlert'); 120 | thealert[0].innerHTML = "Submitting..."; 121 | thealert.show("fast"); 122 | thealert.attr("disabled", true); 123 | }, 124 | 125 | onSuccess: function(formType, result) { 126 | var thealert = $('#insertSuccessAlert'); 127 | if(formType === "update") { 128 | thealert[0].innerHTML = "Appointment Successfully Edited."; 129 | } else { 130 | thealert[0].innerHTML = "New Appointment Created."; 131 | } 132 | //thealert.removeClass('alert-danger alert-info alert-info alert-success'); 133 | //thealert.addClass('alert-success'); 134 | //thealert.removeClass('bg-success'); 135 | //this.resetForm(); 136 | //closeTimeout = Meteor.setTimeout(function() { 137 | // $('#insertSuccessAlert').hide("slow"); 138 | Session.set("changedAppointmentID", result); 139 | goHome(); 140 | //}, 3000); 141 | }, 142 | //docToForm: function(doc){ 143 | // console.log('running docToForm on route: '+Router.current().route.getName()); 144 | // if (doc.date instanceof Date) { 145 | // doc.time = moment(doc.date).format("h:mm A"); 146 | // } 147 | // return doc; 148 | //}, 149 | formToDoc: function(doc) { 150 | doc.providerName = Session.get("selectedProviderName"); 151 | return doc; 152 | }, 153 | formToModifier: function(doc) { 154 | doc.$set.providerName = Session.get("selectedProviderName"); 155 | return doc; 156 | }, 157 | onError: function(formtype, error) { 158 | //console.log('running onError!'); 159 | $('#saveAppointChanges').attr("disabled", false); 160 | var alert = $('#insertSuccessAlert'); 161 | alert.removeClass('alert-danger alert-info alert-info alert-success'); 162 | alert.addClass('alert-danger'); 163 | alert[0].innerHTML = "Uh-oh, something went wrong!"; 164 | alert.show("fast"); 165 | Meteor.setTimeout(function() { 166 | $('#insertSuccessAlert').hide("slow"); 167 | }, 3000); 168 | }, 169 | after: { 170 | insert: function(error, result) {//TODO: When appointment is made, use the data-id var 171 | //console.log('running after insert!'); 172 | //to find it in the appointment list and bounce it! 173 | if (error) { 174 | console.log("Insert Error:", error); 175 | //$("#insertSuccessAlert").alert(); 176 | } else { 177 | console.log("Insert Result:", result); 178 | } 179 | }, 180 | update: function(error, result) { 181 | //console.log('running after insert!'); 182 | //to find it in the appointment list and bounce it! 183 | if (error) { 184 | console.log("update Error:", error); 185 | //$("#insertSuccessAlert").alert(); 186 | } else { 187 | console.log("update Result:", result); 188 | } 189 | } 190 | } 191 | } 192 | }); -------------------------------------------------------------------------------- /client/views/appointments/bookingTable.js: -------------------------------------------------------------------------------- 1 | Template.bookingTable.helpers({ 2 | unusualDays: function() { 3 | return unusualDays.findOne({date: Session.get("date"), providerName: Session.get("selectedProviderName")}); 4 | }, 5 | day: function() { 6 | var momentobj = moment(Session.get("date")); 7 | var ret = momentobj.format("dddd, MMMM Do GGGG"); 8 | return ret + " -"+ dayDelta(Session.get("date")); 9 | }, 10 | bookingTableWrapperStyle: function() { 11 | if (Router.current().route.getName() !== "printout") { 12 | return {style: "height:700px; overflow-y:auto;"} 13 | } 14 | }, 15 | notPrintout: function() { 16 | return Router.current().route.getName() !== "printout"; 17 | }, 18 | times: function(){ 19 | if (Roles.userIsInRole(Meteor.userId(), "provider")) { 20 | console.log("user is provider, setting selected provider name"); 21 | Session.set("selectedProviderName", Meteor.user().providerName); 22 | } 23 | var provObject = getProvObject(Session.get("date"), Session.get('selectedProviderName')); 24 | if (!provObject) { 25 | console.log("provider not yet available, bailing out"); 26 | return; 27 | } 28 | // console.log(provObject); 29 | var dateCounter = moment().startOf('day').hours(provObject.startTime); 30 | var dateTarget = moment().startOf('day').hours(provObject.endTime); 31 | var ret = []; 32 | var theTime; 33 | while(dateTarget.diff(dateCounter) > 0) 34 | { 35 | theTime = dateCounter.format("h:mm A"); 36 | ret.push({time: theTime, rowTimeId:theTime}); 37 | dateCounter.add(provObject.appointmentLength, "minutes"); 38 | } 39 | var finalTime = dateCounter.format("h:mm A"); 40 | // console.log(JSON.stringify({time: finalTime})); 41 | ret.push({time: finalTime, rowTimeId:finalTime}); 42 | return ret; 43 | }, 44 | 45 | blockouts: function() { 46 | return getBlockouts(Session.get("selectedProviderName"), Session.get('date')); 47 | }, 48 | appointments: function() { 49 | var theDate = Session.get("date"); 50 | startDate = moment(theDate).startOf("day").toDate(); 51 | endDate = moment(theDate).endOf("day").toDate(); 52 | // console.log(JSON.stringify({date: {$gte: startDate, $lt: endDate}})); 53 | queryPointer = appointmentList.find({date: {$gte: startDate, $lt: endDate}, 54 | providerName: Session.get("selectedProviderName")}); 55 | return queryPointer; 56 | }, 57 | providerNames: function() { 58 | return providers.find({}, {fields: {name: 1}}) 59 | }, 60 | selected: function() { 61 | if(Session.get("selectedProviderName") === this.name) { 62 | return "active"; 63 | } 64 | }, 65 | todaysUnusualTimes: function () { 66 | return unusualDays.findOne({date:Session.get('date'), providerName: Session.get("selectedProviderName")}) 67 | }, 68 | 69 | buttonStyle: function() { 70 | 71 | if (unusualDays.findOne({date:Session.get('date'), providerName: Session.get("selectedProviderName")})){ 72 | return "display: none;"; 73 | } 74 | else { 75 | return ""; 76 | } 77 | }, 78 | notes: function () { 79 | try{ 80 | return unusualDays.findOne({date:Session.get('date'), providerName: Session.get("selectedProviderName")}).notes 81 | } catch(e) {/*fails when there is no unusualDay for today.*/} 82 | }, 83 | noneSelected: function() { 84 | return Session.get('selectedProviderName') === undefined || !Session.get('selectedProviderName') 85 | 86 | } 87 | 88 | }); 89 | 90 | 91 | 92 | Template.bookingTable.events({ 93 | 'click .providerTab': function(event) { 94 | event.preventDefault(); 95 | console.log($(event.currentTarget).data("name")); 96 | changeParams({providerName: $(event.currentTarget).data("name")}); 97 | }, 98 | 'dblclick .appointmentItem': function(event) { 99 | event.stopImmediatePropagation(); 100 | Router.go('editAppointment', {id: $(event.currentTarget).data("id")}); 101 | }, 102 | 'dblclick .blockoutItem': function(event) { 103 | event.stopImmediatePropagation(); 104 | try{ 105 | if($(event.currentTarget).data("id")) { 106 | Router.go('editBlockout', {id: $(event.currentTarget).data("id")}); 107 | return; 108 | } 109 | } catch (e) {} 110 | if(confirm('This is a repeating blockout, and must be edited from the providers menu. Go there?')){ 111 | Router.go('/providers'); 112 | } 113 | 114 | }, 115 | 'click #customTimesButton': function(event) { 116 | var provObject = providers.findOne({name: Session.get("selectedProviderName")}); 117 | unusualDays.insert({date: Session.get('date'), 118 | providerName: Session.get("selectedProviderName"), 119 | startTime: provObject.startTime, 120 | endTime: provObject.endTime, 121 | appointmentLength: provObject.appointmentLength}); 122 | }, 123 | 'click #deleteCustomTimes': function(event) { 124 | unusualDays.remove(unusualDays.findOne({date:Session.get('date'), providerName: Session.get("selectedProviderName")})._id); 125 | }, 126 | 'dblclick td.rowContent': function(event) { 127 | if (Router.current().route.getName() === "newBlockoutForm") { 128 | newAppointment(event.currentTarget.id, true); 129 | } else { 130 | newAppointment(event.currentTarget.id); 131 | } 132 | } 133 | }); 134 | 135 | Template.bookingTable.created = function() { 136 | Session.set('timesRendered', false); 137 | }; 138 | 139 | Template.bookingTable.rendered = function() { 140 | //console.log("rerendering"); 141 | //rerenderDep.changed(); 142 | fillJqueryCache(); 143 | Session.set('timesRendered', true); 144 | Tracker.autorun(function() { 145 | // /appointToScrollTo 146 | // var pos = $('div[data-id="'+Session.get("currentlyEditingDoc")+'"]')[0].offsetTop 147 | var pos = Session.get("scrollToPoint"); 148 | if (pos === null || typeof pos === "undefined") {return;} 149 | console.log("Scrolling to :" + pos); 150 | $("#bookingTableWrapper").animate({ 151 | scrollTop: pos, 152 | scrollLeft: 0 153 | }); 154 | Tracker.nonreactive(function() { 155 | Session.set("scrollToPoint", null); 156 | }) 157 | 158 | }) 159 | }; 160 | 161 | Template.bookingTable.onDestroyed(function() { 162 | jquerycache = {}; //clear the jquery cache 163 | }); 164 | 165 | 166 | 167 | Template.timeRow.helpers({ 168 | rowHighlightClass: function() { 169 | if (moment(Session.get("newTime"), "hh:mm A").format("h:mm A") == this.time && Session.get("formForInsert") === true) { 170 | //console.log("highlighting row "+ Session.get("newTime")); 171 | return "bg-success"; 172 | } 173 | 174 | } 175 | 176 | }); 177 | Template.timeRow.rendered = function(){ 178 | if(Session.equals("newTime", this.data.time)) { 179 | //console.log("Newtime is : "+ Session.get("newTime")); 180 | Session.set("scrollToPoint", this.firstNode.offsetTop); 181 | } 182 | }; 183 | 184 | -------------------------------------------------------------------------------- /client/routes/routes.js: -------------------------------------------------------------------------------- 1 | subs = new SubsManager({ 2 | cacheLimit: 20,//number of subs to cache 3 | expireIn: 20//minutes to hold on to subs 4 | }); 5 | 6 | Router.configure({ 7 | // layoutTemplate: 'masterLayout', 8 | notFoundTemplate: 'notFound', 9 | loadingTemplate: 'loading', 10 | layoutTemplate: 'singlePageMasterLayout' 11 | }); 12 | Router.onBeforeAction(mustBeSignedIn, {except: ['loginPage']}); 13 | function mustBeSignedIn() { 14 | if (Meteor.loggingIn()) { 15 | //console.log("currently logging in"); 16 | this.render('loading'); 17 | } else { 18 | user = Meteor.user(); 19 | if (!user) { 20 | //console.log("need to log in"); 21 | console.log(Router.current().route.getName()); 22 | this.render("loginPage"); 23 | //Router.go('loginPage', {redirect: Router.current().route.path()}); 24 | } else { 25 | this.next(); 26 | } 27 | } 28 | } 29 | Router.onBeforeAction(correctProviderName, {except: ['loginPage']}); 30 | function correctProviderName() { 31 | if (Roles.userIsInRole(Meteor.user(), "provider") && Meteor.user().providerName !== Session.get("selectedProviderName")) { 32 | Session.set("selectedProviderName",Meteor.user().providerName); 33 | } 34 | this.next(); 35 | } 36 | Router.onBeforeAction(cleanupTimer); 37 | function cleanupTimer() { 38 | //if (typeof closeTimeout !== "undefined") {//ensure time and alert is goneburgers on new route changes. 39 | // $('#saveAppointChanges').attr("disabled", false); 40 | // Meteor.clearTimeout(closeTimeout); 41 | // $('#insertSuccessAlert').hide('fast'); 42 | //} 43 | this.next(); 44 | } 45 | 46 | returnStandardSubs = function(date, providerName, appntId, blockId) { 47 | //date should be a string in YYYY-MM-DD format 48 | if (!providers.findOne({name: providerName})) { 49 | providerName = providers.findOne().name; 50 | //throw new Meteor.Error("returnStandardSubs given invalid providerName", providerName); 51 | } 52 | var thedate = moment(date, 'YYYY-MM-DD').startOf('day').toDate(); 53 | var list = []; 54 | if (typeof date === "string" && typeof providerName === "string") { 55 | Session.set("date", thedate); 56 | Session.set("selectedProviderName", providerName); 57 | list = list.concat([Meteor.subscribe('appointmentList', Session.get('date'), Session.get("selectedProviderName")), 58 | Meteor.subscribe("unusualDays", Session.get("date")), 59 | Meteor.subscribe('blockouts', Session.get('date'), Session.get("selectedProviderName"))]); 60 | } 61 | if (typeof appntId === "string") { 62 | list = list.concat(Meteor.subscribe('singleAppoint', appntId)); 63 | } else if (typeof blockId === "string") { 64 | list = list.concat(Meteor.subscribe('singleBlockout', blockId)); 65 | } 66 | //console.log(list); 67 | return list; 68 | 69 | 70 | 71 | }; 72 | 73 | Tracker.autorun(function() { 74 | console.log("session date has changed! " + Session.get('date')); 75 | }); 76 | Router.route('index', { 77 | path: '/', 78 | action: function() { 79 | if (this.ready()) { 80 | Router.go('bookingTable', 81 | {date: moment().startOf('day').format('YYYY-MM-DD'), 82 | providerName: providers.findOne().name}); 83 | } 84 | } 85 | }); 86 | 87 | 88 | Router.route('newAppointment', { 89 | path: '/new/:date/:providerName/:time?', 90 | layoutTemplate: "sideEditMasterTemplate", 91 | template: 'appointmentEdit', 92 | loadingTemplate: 'loading', 93 | waitOn: function() { 94 | if (Meteor.user()) { 95 | console.log("NewAppointment here, grabbing my standard subs!"); 96 | return returnStandardSubs(this.params.date, this.params.providerName, null, null); 97 | } 98 | 99 | }, 100 | onBeforeAction: function () { 101 | console.log("new onbeforeaction"); 102 | 103 | Session.set("formForInsert", true); 104 | Session.set("currentlyEditingDoc", null); 105 | 106 | if (this.params.time) { 107 | Session.set("newTime", this.params.time.replace('-', ':').replace('-', ' ')); 108 | } 109 | this.next(); 110 | }, 111 | action: function() { 112 | console.log("newAppointment action"); 113 | if(this.ready()) { 114 | console.log("newAppointment ready"); 115 | this.render('bookingTable', {to: "right"}); 116 | this.render(); 117 | } 118 | }, 119 | onStop: function() { 120 | Session.set("newTime", null);//remove Highlight 121 | } 122 | }); 123 | Router.route('editAppointment', { 124 | path: '/edit/:id', 125 | layoutTemplate: "sideEditMasterTemplate", 126 | template: 'appointmentEdit', 127 | //waitOn: function() { 128 | // return returnStandardSubs(null, null, this.params.id, null); 129 | //}, 130 | loadingTemplate: 'loading', 131 | onBeforeAction: function () { 132 | console.log("edit onbeforeaction"); 133 | var handle = Meteor.subscribe('singleAppoint', this.params.id); 134 | if (handle.ready()) { 135 | var appoint = appointmentList.findOne(this.params.id); 136 | if (!appoint) {this.render("notFound")} 137 | Session.set('date', moment(appoint.date).startOf('day').toDate()); 138 | Session.set('selectedProviderName', appoint.providerName); 139 | Tracker.autorun(function() { 140 | var subs = returnStandardSubs(moment(Session.get('date')).startOf('day').format('YYYY-MM-DD'), 141 | Session.get('selectedProviderName'), 142 | null, 143 | null); 144 | }); 145 | //this.wait(subs); 146 | Session.set("formForInsert", false); 147 | Session.set("currentlyEditingDoc", this.params.id); 148 | this.next(); 149 | } 150 | 151 | 152 | 153 | }, 154 | action: function() { 155 | if(this.ready()) { 156 | this.render('bookingTable', {to: 'right'}); 157 | this.render(); 158 | } 159 | }, 160 | onAfterAction: function() { 161 | //console.log("edit onafteraction"); 162 | }, 163 | onStop: function() { 164 | //console.log("edit onstop"); 165 | Session.set("formForInsert", true); 166 | Session.set("currentlyEditingDoc", null); 167 | } 168 | }); 169 | Router.route('newBlockout', { 170 | path: '/newBlockout/:date/:providerName/:time', 171 | layoutTemplate: "sideEditMasterTemplate", 172 | template: 'blockoutEdit', 173 | waitOn: function() { 174 | //console.log("newBlockout here, grabbing my standard subs!"); 175 | if (Meteor.user()) { 176 | return returnStandardSubs(this.params.date, this.params.providerName, null, null); 177 | } 178 | }, 179 | onBeforeAction: function() { 180 | Session.set("formForInsert", true); 181 | Session.set("currentlyEditingDoc", null); 182 | if (this.params.time) { 183 | Session.set("newTime", this.params.time.replace('-', ':').replace('-', ' ')); 184 | } 185 | this.next(); 186 | }, 187 | action: function() { 188 | this.render('bookingTable', {to: "right"}); 189 | this.render(); 190 | }, 191 | onStop: function() { 192 | Session.set("newTime", null);//remove Highlight 193 | } 194 | 195 | }); 196 | Router.route('editBlockout', { 197 | path: '/editBlockout/:id', 198 | layoutTemplate: "sideEditMasterTemplate", 199 | template: 'blockoutEdit',//TODO: If not on correct date for appointment, change 200 | loadingTemplate: 'loading', 201 | onBeforeAction: function () { 202 | var handle = Meteor.subscribe('singleBlockout', this.params.id); 203 | Session.set("formForInsert", false); 204 | Session.set("currentlyEditingDoc", this.params.id); 205 | if (handle.ready()) { 206 | Tracker.autorun(function () { 207 | var block = blockouts.findOne(Session.get('currentlyEditingDoc')); 208 | var subs = returnStandardSubs(moment(block.date).startOf('day').format('YYYY-MM-DD'), 209 | block.providerName, 210 | null, 211 | null); 212 | }); 213 | this.next(); 214 | } 215 | }, 216 | action: function() { 217 | if(this.ready()) { 218 | this.render('bookingTable', {to: 'right'}); 219 | this.render(); 220 | } 221 | }, 222 | //onAfterAction: function() { 223 | // if (this.ready()) { 224 | // 225 | // } 226 | //}, 227 | onStop: function() { 228 | Session.set("formForInsert", true); 229 | Session.set("currentlyEditingDoc", null); 230 | } 231 | }); 232 | Router.route('providerList', { 233 | path: '/providers' 234 | }); 235 | Router.route('userList', { 236 | path: '/users', 237 | waitOn: function() { 238 | if(Meteor.user()) { 239 | return Meteor.subscribe("userList"); 240 | } 241 | } 242 | }); 243 | 244 | Router.route('loginPage', { 245 | path: '/login/(.*)', 246 | template: 'loginPage', 247 | onBeforeAction: function() { 248 | if(this.params) { 249 | Session.set('loginRedirect', this.params[0]); 250 | } 251 | //console.log(this.params[0]); 252 | this.next(); 253 | } 254 | }); 255 | 256 | Router.route('printout', { 257 | template: "bookingTable", 258 | layoutTemplate: "blank", 259 | path: '/printout/:date/:providerName', 260 | waitOn: function() { 261 | if(Meteor.user()) { 262 | return returnStandardSubs(this.params.date, this.params.providerName); 263 | } 264 | }, 265 | action: function() { 266 | if(this.ready()) { 267 | this.render(); 268 | } 269 | } 270 | }); 271 | 272 | Router.route('calendar', { 273 | template:"calendar", 274 | path: '/calendar/:year/:month', 275 | waitOn: function() { 276 | if (Meteor.user()) { 277 | var startDate = moment().year(this.params.year).month(this.params.month).startOf('month').subtract(5, "days"); 278 | var endDate = moment().year(this.params.year).month(this.params.month).endOf('month').add(10, "days"); 279 | return Meteor.subscribe("unusualDaysRange", startDate.toDate(), endDate.toDate()); 280 | } 281 | }, 282 | onBeforeAction: function() { 283 | Session.set("calendarStart", moment().year(this.params.year).month(this.params.month).startOf('month').toDate()); 284 | Session.set("calendarEnd", moment().year(this.params.year).month(this.params.month).endOf('month').toDate()); 285 | Session.set("date", moment().year(this.params.year).month(this.params.month).startOf('month').toDate()); 286 | this.next(); 287 | } 288 | }); 289 | 290 | Router.route('bookingTable', { 291 | path: '/:date/:providerName', 292 | waitOn: function() { 293 | if(Meteor.user()) { 294 | return returnStandardSubs(this.params.date, this.params.providerName); 295 | } 296 | }, 297 | onBeforeAction: function () { 298 | Session.setDefault("formForInsert", true); 299 | AutoForm.resetForm("insertAppointmentFormInner"); 300 | AutoForm.resetForm("insertBlockoutFormInner"); 301 | Session.set("newTime", null); 302 | this.next(); 303 | }, 304 | action: function() { 305 | if(this.ready()) { 306 | this.render(); 307 | } 308 | } 309 | }); 310 | 311 | 312 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------