├── .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 |
2 | Loading.
3 |
--------------------------------------------------------------------------------
/client/views/notFound.html:
--------------------------------------------------------------------------------
1 |
2 | 404 not found.
3 |
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 |
2 |
3 | {{> atForm}}
4 |
5 |
--------------------------------------------------------------------------------
/client/views/printouts/simplePrintoutGenerator.html:
--------------------------------------------------------------------------------
1 |
2 | {{#for appointmentItem}}
3 |
4 | {{/for}}
5 |
--------------------------------------------------------------------------------
/client/views/printouts/printoutGenerator.html:
--------------------------------------------------------------------------------
1 |
2 | {{bookingTable}}
3 |
4 |
5 |
6 | {{> yield}}
7 |
--------------------------------------------------------------------------------
/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 |
4 |
5 |
6 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/client/views/appointments/sideEditWrapper.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> navbar}}
4 |
5 |
6 |
7 | {{> yield}}
8 |
9 |
10 | {{> yield "right"}}
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/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 |
2 |
6 |
7 | {{title}}
{{{inbetween}}}
8 |
9 | {{time}} {{{inbetween}}} {{length}} minutes
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/views/datepicker.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/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 |
2 | {{#autoForm collection=users id="registrationForm" collection="Meteor.users"}}
3 |
11 | {{/autoForm}}
12 |
--------------------------------------------------------------------------------
/client/views/providers/providerList.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
{{> providerEdit}}
15 |
16 |
--------------------------------------------------------------------------------
/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 |
2 | {{# autoForm collection="providers" doc=editingProvider id="updateProviderForm" type="update"}}
3 |
12 | {{/autoForm}}
13 |
--------------------------------------------------------------------------------
/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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
2 |
3 |
4 | {{firstname}} {{lastname}}
{{{inbetween}}}
5 |
6 | {{time}} - {{length}} minutes
7 | {{#if phone}}
8 | {{{inbetween}}}
9 |
10 |
11 | {{phone}}
12 |
13 | {{/if}}
14 | {{#if notes}}
15 | {{{inbetween}}}
16 |
17 | {{notes}}
18 | {{/if}}
19 |
20 |
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 |
19 |
20 |
--------------------------------------------------------------------------------
/client/views/calendarview/calendar.html:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 | {{> fullcalendar id="innercalendar" timezone="Pacific/Auckland" header=false}}
17 |
18 |
--------------------------------------------------------------------------------
/.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 |
2 |
3 | {{> insertBlockoutForm}}
4 |
5 |
6 |
7 |
8 | {{#autoForm collection="blockouts" doc=currentDoc type=currentType id="insertBlockoutFormInner"}}
9 | {{title}}
10 |
11 |
17 |
22 | {{/autoForm}}
23 |
--------------------------------------------------------------------------------
/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 |
2 |
3 | {{> insertAppointmentForm}}
4 |
5 |
6 |
7 |
8 | {{#autoForm collection=appointmentList doc=currentDoc id="insertAppointmentFormInner" type=currentType}}
9 | {{title}}
10 |
11 |
20 |
25 | {{/autoForm}}
26 |
--------------------------------------------------------------------------------
/client/views/users/userManagement.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{#if userIsAdmin}}
4 |
17 | {{/if}}
18 |
{{> userEdit}}
19 |
20 |
21 |
22 |
23 | {{provoptions}}
24 | {{# autoForm collection=usercoll doc=editingUser id="updateUserForm" type="update"}}
25 |
39 | {{/autoForm}}
40 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
5 |
{{day}}
6 |
{{notes}}
7 |
8 |
17 |
18 |
19 |
20 |
21 | |
22 | Time
23 | |
24 |
25 | Appointments
26 | |
27 |
28 |
29 |
30 | {{#each times}}
31 | {{> timeRow}}
32 | {{/each}}
33 |
34 | {{#each blockouts}}
35 | {{> blockoutItem}}
36 | {{/each}}
37 | {{#each appointments}}
38 | {{> appointmentItem}}
39 | {{/each}}
40 |
41 |
42 |
43 |
44 |
45 | {{#if notPrintout}}
46 |
47 |
48 | {{#if unusualDays}}
49 | {{#autoForm collection="unusualDays" id="changeDayTimes"
50 | doc=todaysUnusualTimes type="update" autosave=true class="form-inline"}}
51 |
52 | {{> afQuickField name="startTime" style="width: 75px"}}
53 | {{> afQuickField name="endTime" style="width: 75px"}}
54 | {{> afQuickField name="appointmentLength" step=5 style="width: 75px"}}
55 | {{> afQuickField name="notes" }}
56 |
57 |
58 | {{/autoForm}}
59 | {{/if}}
60 |
61 | {{/if}}
62 |
63 |
64 |
65 |
66 |
67 |
68 | |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/client/booking.html:
--------------------------------------------------------------------------------
1 |
2 | Booking
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{> navbar}}
13 |
14 | {{> yield}}
15 |
16 |
17 |
18 |
19 |
20 |
59 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------