Ολοκλήρωση του πρώτου μέρους δυνατοτήτων της σελίδας Calendar του backend. Σχεδίαση και προετοιμασία του τρόπου με τον οποίο θα εκτελείται η διαδικασία OAuth, έτσι ώστε να συχρονίζονται τα πλάνα των πάροχων με το Google Calendar.

This commit is contained in:
alextselegidis@gmail.com 2013-06-18 16:06:34 +00:00
parent d2eb0b6400
commit fc53817e81
16 changed files with 2797 additions and 157 deletions

View file

@ -1,9 +1,15 @@
VERSION 0.2 VERSION 0.3
=========== ===========
- Use the PHPMailer class for sending HTML emails. Main
- Display error message to users.
- Includes complete Google Sync protocol document. - First backend-calendar page implementation (not complete, admin's perspective).
- Customers can book appointments only for the available hours. - Javascript Google API usage from the customer's perspective.
- Generation of code documentation. - Backend google calendar authentication Process.
- Customers can edit the application through unique links (from their emails). - Sync every appointment change made from E!A to Google Calendar.
- Minor Fixes - Display user friendly error messages.
Minor
- Added sync exception to Google Sync library.

View file

@ -1,36 +0,0 @@
#! /bin/bash
clear
echo "========================================="
echo " "
echo "Easy!Appointments Installation Script "
echo " "
echo "========================================="
printf "Before you continue ensure that your MAMP
\nserver is running and you already know the
\nfolder path in which the application is
\ngoing to be installed.\n\n"
read -p "Press Enter to Continue" -n 1
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
# Copy Application Files
printf "\n\nEnter destination directory:"
read DEST
cp -r "easy_appointments" $DEST
# Install MySQL Database
printf "\n>>>Installing database ..."
/Applications/MAMP/Library/bin/mysql -uroot -p
/Applications/MAMP/Library/bin/mysql "CREATE DATABASE IF NOT EXISTS easy_appointments"
/Applications/MAMP/Library/bin/mysql "easy_appointments" < "easy_appointments.sql"
printf "\n\n>>>Review your configuration.php file before trying to use the application."
# End of install operation
printf "\n\n======================================"
printf "\nInstallation completed successfully!"
read
fi

View file

@ -28,19 +28,19 @@ class Backend extends CI_Controller {
} }
public function customers() { public function customers() {
echo '<h1>Not implemented yet.</h1>';
} }
public function services() { public function services() {
echo '<h1>Not implemented yet.</h1>';
} }
public function providers() { public function providers() {
echo '<h1>Not implemented yet.</h1>';
} }
public function settings() { public function settings() {
echo '<h1>Not implemented yet.</h1>';
} }
/** /**
@ -50,8 +50,8 @@ class Backend extends CI_Controller {
* period and record type (provider or service). * period and record type (provider or service).
* *
* @param {numeric} $_POST['record_id'] Selected record id. * @param {numeric} $_POST['record_id'] Selected record id.
* @param {string} $_POST['filter_type'] Could be either FILTER_TYPE_PROVIDER or * @param {string} $_POST['filter_type'] Could be either FILTER_TYPE_PROVIDER
* FILTER_TYPE_SERVICE. * or FILTER_TYPE_SERVICE.
* @param {string} $_POST['start_date'] The user selected start date. * @param {string} $_POST['start_date'] The user selected start date.
* @param {string} $_POST['end_date'] The user selected end date. * @param {string} $_POST['end_date'] The user selected end date.
*/ */
@ -86,6 +86,40 @@ class Backend extends CI_Controller {
echo json_encode($appointments); echo json_encode($appointments);
} }
/**
* [AJAX] Save appointment changes that are made from the backend calendar
* page.
*
* @param array $_POST['appointment_data'] (OPTIONAL) Array with the
* appointment data.
* @param array $_POST['customer_data'] (OPTIONAL) Array with the customer
* data.
*/
public function ajax_save_appointment_changes() {
try {
if (isset($_POST['appointment_data'])) {
$appointment_data = json_decode(stripcslashes($_POST['appointment_data']), true);
$this->load->model('Appointments_Model');
$this->Appointments_Model->add($appointment_data);
}
if (isset($_POST['customer_data'])) {
$customer_data = json_decode(stripcslashes($_POST['customer_data']), true);
$this->load->model('Customers_Model');
$this->Customers_Model->add($customer_data);
}
echo json_encode('SUCCESS');
} catch(Exception $exc) {
$js_error = array(
'error' => $exc->getMessage()
);
echo json_encode($js_error);
}
}
} }
/* End of file backend.php */ /* End of file backend.php */

View file

@ -0,0 +1,40 @@
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Google extends CI_Controller {
/**
* Authorize Google Calendar API usage for a specific provider.
*
* Since it is required to follow the web application flow, in order to retrieve
* a refresh token from the Google API service, this method is going to authorize
* the given provider.
*
* @param int $provider_id The provider id, for whom the sync authorization is
* made.
*/
public function oauth($provider_id) {
$this->load->library('Google_Sync');
// @task Create auth link and redirect browser window.
}
/**
* Callback method for the Google Calendar API authorization process.
*
* Once the user grants consent with his Google Calendar data usage, the Google
* OAuth service will redirect him back in this page. Here we are going to store
* the refresh token, because this is what will be used to generate access tokens
* in the future.
*
* <strong>IMPORTANT!</strong> Because it is necessary to authorize the application
* using the web server flow (see official documentation of OAuth), every
* Easy!Appointments installation should use its own calendar api key. So in every
* api console account, the "http://path-to-e!a/google/oauth_callback" should be
* included in the allowed redirect urls.
*/
public function oauth_callback() {
// @task Store refresh token.
}
}
/* End of file google.php */
/* Location: ./application/controllers/google.php */

View file

@ -4,6 +4,9 @@
<script type="text/javascript" <script type="text/javascript"
src="<?php echo $base_url; ?>assets/js/libs/jquery/fullcalendar.min.js"></script> src="<?php echo $base_url; ?>assets/js/libs/jquery/fullcalendar.min.js"></script>
<script type="text/javascript"
src="<?php echo $base_url; ?>assets/js/libs/jquery/jquery-ui-timepicker-addon.js"></script>
<script type="text/javascript" <script type="text/javascript"
src="<?php echo $base_url; ?>assets/js/backend_calendar.js"></script> src="<?php echo $base_url; ?>assets/js/backend_calendar.js"></script>
@ -39,9 +42,15 @@
Enable Sync Enable Sync
</button> </button>
<button id="insert-new-appointment" class="btn"
title="Create a new appointment and store it into the database.">
<i class="icon-plus"></i>
New Appointment
</button>
<button id="insert-unavailable-period" class="btn" <button id="insert-unavailable-period" class="btn"
title="During unavailalbe period the provider won't accept new appointments."> title="During unavailalbe period the provider won't accept new appointments.">
<i class="icon-plus"></i> <i class="icon-ban-circle"></i>
Unavailable Unavailable
</button> </button>
</div> </div>
@ -62,6 +71,8 @@
<fieldset> <fieldset>
<legend>Appointment Details</legend> <legend>Appointment Details</legend>
<input id="appointment-id" type="hidden" />
<div class="control-group"> <div class="control-group">
<label for="select-service" class="control-label">Service</label> <label for="select-service" class="control-label">Service</label>
<div class="controls"> <div class="controls">
@ -77,9 +88,16 @@
</div> </div>
<div class="control-group"> <div class="control-group">
<label for="select-date" class="control-label"></label> <label for="start-datetime" class="control-label">Start Date/Time</label>
<div class="controls"> <div class="controls">
<span id="select-date"></span> <input type="text" id="start-datetime" />
</div>
</div>
<div class="control-group">
<label for="end-datetime" class="control-label">End Date/Time</label>
<div class="controls">
<input type="text" id="end-datetime" />
</div> </div>
</div> </div>
</fieldset> </fieldset>
@ -87,6 +105,8 @@
<fieldset> <fieldset>
<legend>Customer Details</legend> <legend>Customer Details</legend>
<input id="customer-id" type="hidden" />
<div class="control-group"> <div class="control-group">
<label for="first-name" class="control-label">First Name</label> <label for="first-name" class="control-label">First Name</label>
<div class="controls"> <div class="controls">

View file

@ -0,0 +1,7 @@
<?php
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
?>

View file

@ -30,6 +30,10 @@
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
href="<?php echo $base_url; ?>assets/css/backend.css"> href="<?php echo $base_url; ?>assets/css/backend.css">
<link
rel="stylesheet"
type="text/css"
href="<?php echo $base_url; ?>assets/css/general.css">
<?php <?php
// ------------------------------------------------------------ // ------------------------------------------------------------
@ -104,3 +108,4 @@
</div> </div>
</div> </div>
<div id="notification" style="display: none;"></div>

View file

@ -0,0 +1,7 @@
<?php
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
?>

View file

@ -0,0 +1,7 @@
<?php
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
?>

View file

@ -0,0 +1,7 @@
<?php
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
?>

View file

@ -30,6 +30,8 @@ root {
} }
#footer #footer-content { padding: 15px; } #footer #footer-content { padding: 15px; }
#notification strong { margin-right: 15px; }
/* BACKEND CALENDAR PAGE /* BACKEND CALENDAR PAGE
-------------------------------------------------------------------- */ -------------------------------------------------------------------- */
#calendar-page #calendar-toolbar { margin: 15px 10px 20px 10px; padding-bottom: 10px; #calendar-page #calendar-toolbar { margin: 15px 10px 20px 10px; padding-bottom: 10px;
@ -38,7 +40,7 @@ root {
#calendar-page #calendar-filter label { display: inline-block; margin-right: 7px; #calendar-page #calendar-filter label { display: inline-block; margin-right: 7px;
font-weight: bold; font-size: 18px; } font-weight: bold; font-size: 18px; }
#calendar-page #calendar-filter select { margin-top: 5px; } #calendar-page #calendar-filter select { margin-top: 5px; }
#calendar-page #calendar-actions { display: inline-block; float: right; } #calendar-page #calendar-actions { display: inline-block; float: right; margin-top: 4px; }
#calendar-page #calendar { margin: 12px; } #calendar-page #calendar { margin: 12px; }
/* BACKEND CUSTOMERS PAGE /* BACKEND CUSTOMERS PAGE

View file

@ -0,0 +1,12 @@
/* JQUERY UI DATETIME PICKER ADDON
------------------------------------------------------------------------- */
.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
.ui-timepicker-div dl { text-align: left; }
.ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; }
.ui-timepicker-div dl dd { margin: 0 10px 10px 65px; }
.ui-timepicker-div td { font-size: 90%; }
.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
.ui-timepicker-rtl{ direction: rtl; }
.ui-timepicker-rtl dl { text-align: right; }
.ui-timepicker-rtl dl dd { margin: 0 65px 10px 10px; }

View file

@ -32,5 +32,43 @@ var Backend = {
'position' : 'static' 'position' : 'static'
}); });
} }
},
/**
* Display backend notifications to user.
*
* Using this method you can display notifications to the use with custom
* messages. If the 'actions' array is provided then an action link will
* be displayed too.
*
* @param {string} message Notification message
* @param {array} actions An array with custom actions that will be available
* to the user. Every array item is an object that contains the 'label' and
* 'function' key values.
*/
displayNotification: function(message, actions) {
if (message === undefined) {
message = 'NO MESSAGE PROVIDED FOR THIS NOTIFICATION';
}
var notificationHtml =
'<div class="notification alert">' +
'<strong>' + message + '</strong>';
$.each(actions, function(index, action) {
var actionId = action['label'].toLowerCase().replace(' ', '-');
notificationHtml += '<button id="' + actionId + '" class="btn">'
+ action['label'] + '</button>';
$(document).off('click', '#' + actionId);
$(document).on('click', '#' + actionId, action['function']);
});
notificationHtml += '</div>';
var leftValue = window.innerWidth / 2 - $('body .notification').width() - 200;
$('#notification').html(notificationHtml);
$('#notification').show('blind');
} }
} };

View file

@ -4,10 +4,12 @@
* @namespace BackendCalendar * @namespace BackendCalendar
*/ */
var BackendCalendar = { var BackendCalendar = {
// :: NAMESPACE CONSTANTS
FILTER_TYPE_PROVIDER : 'provider', FILTER_TYPE_PROVIDER : 'provider',
FILTER_TYPE_SERVICE : 'service', FILTER_TYPE_SERVICE : 'service',
lastFocusedEvent : undefined, // Contain event data for later use. // :: NAMESPACE VALIABLES
lastFocusedEventData : undefined, // Contain event data for later use.
/** /**
* This function makes the necessary initialization for the default backend * This function makes the necessary initialization for the default backend
@ -17,7 +19,7 @@ var BackendCalendar = {
* @param {bool} defaultEventHandlers (OPTIONAL = TRUE) Determines whether the * @param {bool} defaultEventHandlers (OPTIONAL = TRUE) Determines whether the
* default event handlers will be set for the current page. * default event handlers will be set for the current page.
*/ */
initialize: function(defaultEventHandlers) { initialize : function(defaultEventHandlers) {
if (defaultEventHandlers === undefined) defaultEventHandlers = true; if (defaultEventHandlers === undefined) defaultEventHandlers = true;
// :: INITIALIZE THE DOM ELEMENTS OF THE PAGE // :: INITIALIZE THE DOM ELEMENTS OF THE PAGE
@ -25,6 +27,7 @@ var BackendCalendar = {
defaultView : 'agendaWeek', defaultView : 'agendaWeek',
height : BackendCalendar.getCalendarHeight(), height : BackendCalendar.getCalendarHeight(),
editable : true, editable : true,
slotMinutes : 15,
columnFormat : { columnFormat : {
month : 'ddd', month : 'ddd',
week : 'ddd d/M', week : 'ddd d/M',
@ -40,84 +43,20 @@ var BackendCalendar = {
center : 'title', center : 'title',
right : 'agendaDay,agendaWeek,month' right : 'agendaDay,agendaWeek,month'
}, },
windowResize : function(view) {
$('#calendar').fullCalendar('option', 'height',
BackendCalendar.getCalendarHeight());
},
dayClick : function(date, allDay, jsEvent, view) {
if (allDay) {
// Switch to day view
$('#calendar').fullCalendar('gotoDate', date);
$('#calendar').fullCalendar('changeView', 'agendaDay');
}
},
eventClick : function(event, jsEvent, view) {
// Display a popover with the event details.
var html =
'<style type="text/css">'
+ '.popover-content strong {min-width: 80px; display:inline-block;}'
+ '.popover-content button {margin-right: 10px;}'
+ '</style>' +
'<strong>Start</strong> '
+ event.start.toString('dd-MM-yyyy HH:mm')
+ '<br>' +
'<strong>End</strong> '
+ event.end.toString('dd-MM-yyyy HH:mm')
+ '<br>' +
'<strong>Service</strong> '
+ event.title
+ '<br>' +
'<strong>Provider</strong> '
+ event.data['provider']['first_name'] + ' '
+ event.data['provider']['last_name']
+ '<br>' +
'<strong>Customer</strong> '
+ event.data['customer']['first_name'] + ' '
+ event.data['customer']['last_name']
+ '<hr>' +
'<center>' +
'<button class="edit-popover btn btn-primary">Edit</button>' +
'<button class="close-popover btn" data-po=' + jsEvent.target + '>Close</button>' +
'</center>';
$(jsEvent.target).popover({ // Calendar events need to be declared on initialization.
placement : 'top', windowResize : BackendCalendar.calendarWindowResize,
title : event.title, viewDisplay : BackendCalendar.calendarViewDisplay,
content : html, dayClick : BackendCalendar.calendarDayClick,
html : true, eventClick : BackendCalendar.calendarEventClick,
container : 'body', eventResize : BackendCalendar.calendarEventResize,
trigger : 'manual' eventDrop : BackendCalendar.calendarEventDrop
});
$(jsEvent.target).popover('show');
BackendCalendar.lastFocusedEvent = event;
},
eventResize : function(event, dayDelta, minuteDelta, revertFunc, jsEvent, ui, view) {
// @task Display confirmation modal.
},
eventDrop : function(event, dayDelta, minuteDelta, allDay, revertFunc, jsEvent, ui, view) {
// @task Display confirmation modal.
},
viewDisplay : function(view) {
// Place the footer into correct position because the calendar
// height might change.
BackendCalendar.refreshCalendarAppointments(
$('#calendar'),
$('#select-filter-item').val(),
$('#select-filter-item option:selected').attr('type'),
$('#calendar').fullCalendar('getView').visStart,
$('#calendar').fullCalendar('getView').visEnd);
$(window).trigger('resize');
$('.fv-events').each(function(index, eventHandle) {
$(eventHandle).popover();
});
}
}); });
// Trigger once to set the proper footer position after calendar
// initialization.
BackendCalendar.calendarWindowResize();
// :: FILL THE SELECT ELEMENTS OF THE PAGE // :: FILL THE SELECT ELEMENTS OF THE PAGE
var optgroupHtml = '<optgroup label="Providers">'; var optgroupHtml = '<optgroup label="Providers">';
$.each(GlobalVariables.availableProviders, function(index, provider) { $.each(GlobalVariables.availableProviders, function(index, provider) {
@ -131,8 +70,8 @@ var BackendCalendar = {
optgroupHtml = '<optgroup label="Services">'; optgroupHtml = '<optgroup label="Services">';
$.each(GlobalVariables.availableServices, function(index, service) { $.each(GlobalVariables.availableServices, function(index, service) {
optgroupHtml += '<option value="' + service['id'] + '" ' + optgroupHtml += '<option value="' + service['id'] + '" ' +
'type="' + BackendCalendar.FILTER_TYPE_SERVICE + '">' 'type="' + BackendCalendar.FILTER_TYPE_SERVICE + '">' +
+ service['name'] + '</option>'; service['name'] + '</option>';
}); });
optgroupHtml += '</optgroup>'; optgroupHtml += '</optgroup>';
$('#select-filter-item').append(optgroupHtml) $('#select-filter-item').append(optgroupHtml)
@ -149,9 +88,9 @@ var BackendCalendar = {
* page. If you do not need the default handlers then initialize the page * page. If you do not need the default handlers then initialize the page
* by setting the "defaultEventHandlers" argument to "false". * by setting the "defaultEventHandlers" argument to "false".
*/ */
bindEventHandlers: function() { bindEventHandlers : function() {
/** /**
* Event: Calendar filter item "Changed" * Event: Calendar Filter Item "Change"
* *
* Load the appointments that correspond to the select filter item and * Load the appointments that correspond to the select filter item and
* display them on the calendar. * display them on the calendar.
@ -164,11 +103,20 @@ var BackendCalendar = {
$('#calendar').fullCalendar('getView').visStart, $('#calendar').fullCalendar('getView').visStart,
$('#calendar').fullCalendar('getView').visEnd); $('#calendar').fullCalendar('getView').visEnd);
// @task If current value is service, then the sync buttons must be disabled. // If current value is service, then the sync buttons must be
// disabled.
if ($('#select-filter-item option:selected').attr('type')
=== BackendCalendar.FILTER_TYPE_SERVICE) {
$('#google-sync, #enable-sync, #insert-unavailable-period')
.prop('disabled', true);
} else {
$('#google-sync, #enable-sync, #insert-unavailable-period')
.prop('disabled', false);
}
}); });
/** /**
* Event: Popover close button "Clicked" * Event: Popover Close Button "Click"
* *
* Hides the open popover element. * Hides the open popover element.
*/ */
@ -177,18 +125,19 @@ var BackendCalendar = {
}); });
/** /**
* Event: Popover edit button "Clicked" * Event: Popover Edit Button "Click"
* *
* Enables the edit dialog of the selected calendar event. * Enables the edit dialog of the selected calendar event.
*/ */
$(document).on('click', '.edit-popover', function() { $(document).on('click', '.edit-popover', function() {
$(this).parents().eq(2).remove(); // Hide the popover $(this).parents().eq(2).remove(); // Hide the popover
var appointmentData = BackendCalendar.lastFocusedEvent.data; var appointmentData = BackendCalendar.lastFocusedEventData.data;
var modalHandle = $('#manage-appointment'); var modalHandle = $('#manage-appointment');
// :: APPLY APPOINTMENT DATA AND SHOW TO MODAL DIALOG // :: APPLY APPOINTMENT DATA AND SHOW TO MODAL DIALOG
modalHandle.find('input, textarea').val(''); modalHandle.find('input, textarea').val('');
modalHandle.find('#appointment-id').val(appointmentData['id']);
// Fill the services listbox and select the appointment service. // Fill the services listbox and select the appointment service.
$.each(GlobalVariables.availableServices, function(index, service) { $.each(GlobalVariables.availableServices, function(index, service) {
@ -220,7 +169,25 @@ var BackendCalendar = {
modalHandle.find('#select-provider').val(appointmentData['id_users_provider']); modalHandle.find('#select-provider').val(appointmentData['id_users_provider']);
// Set the start and end datetime of the appointment.\
var startDatetime = Date.parseExact(appointmentData['start_datetime'],
'yyyy-MM-dd HH:mm:ss').toString('dd/MM/yyyy HH:mm');
modalHandle.find('#start-datetime').datetimepicker({
dateFormat : 'dd/mm/yy',
defaultValue: startDatetime
});
modalHandle.find('#start-datetime').val(startDatetime);
var endDatetime = Date.parseExact(appointmentData['end_datetime'],
'yyyy-MM-dd HH:mm:ss').toString('dd/MM/yyyy HH:mm');
modalHandle.find('#end-datetime').datetimepicker({
dateFormat : 'dd/mm/yy',
defaultValue: endDatetime
});
modalHandle.find('#end-datetime').val(endDatetime);
var customerData = appointmentData['customer']; var customerData = appointmentData['customer'];
modalHandle.find('#customer-id').val(appointmentData['id_users_customer']);
modalHandle.find('#first-name').val(customerData['first_name']); modalHandle.find('#first-name').val(customerData['first_name']);
modalHandle.find('#last-name').val(customerData['last_name']); modalHandle.find('#last-name').val(customerData['last_name']);
modalHandle.find('#email').val(customerData['email']); modalHandle.find('#email').val(customerData['email']);
@ -228,16 +195,14 @@ var BackendCalendar = {
modalHandle.find('#address').val(customerData['address']); modalHandle.find('#address').val(customerData['address']);
modalHandle.find('#city').val(customerData['city']); modalHandle.find('#city').val(customerData['city']);
modalHandle.find('#zip-code').val(customerData['zip_code']); modalHandle.find('#zip-code').val(customerData['zip_code']);
modalHandle.find('#notes').val(customerData['notes']); modalHandle.find('#notes').val(appointmentData['notes']);
// :: DISPLAY THE MANAGE APPOINTMENTS MODAL DIALOG // :: DISPLAY THE MANAGE APPOINTMENTS MODAL DIALOG
$('#manage-appointment').modal('show'); $('#manage-appointment').modal('show');
}); });
/** /**
* Event: Manage Appointments Dialog Cancel Button "Clicked" * Event: Manage Appointments Dialog Cancel Button "Click"
* *
* Closes the dialog without making any actions. * Closes the dialog without making any actions.
*/ */
@ -246,16 +211,108 @@ var BackendCalendar = {
}); });
/** /**
* Event: Manage Appointments Dialog Save Button "Clicked" * Event: Manage Appointments Dialog Save Button "Click"
* *
* Stores the appointment changes. * Stores the appointment changes.
*/ */
$('#manage-appointment #save-button').click(function() { $('#manage-appointment #save-button').click(function() {
// :: PREPARE APPOINTMENT DATA FOR AJAX CALL // :: PREPARE APPOINTMENT DATA FOR AJAX CALL
var appointmentData = {}; var modalHandle = $('#manage-appointment');
// Id must exist on the object in order for the model to update
// the record and not to perform an insert operation.
var startDatetime = Date.parseExact(modalHandle.find('#start-datetime').val(),
'dd/MM/yyyy HH:mm').toString('yyyy-MM-dd HH:mm:ss');
var endDatetime = Date.parseExact(modalHandle.find('#end-datetime').val(),
'dd/MM/yyyy HH:mm').toString('yyyy-MM-dd HH:mm:ss');
var appointmentData = {
'id' : modalHandle.find('#appointment-id').val(),
'id_services' : modalHandle.find('#select-service').val(),
'id_users_provider' : modalHandle.find('#select-provider').val(),
'id_users_customer' : modalHandle.find('#customer-id').val(),
'start_datetime' : startDatetime,
'end_datetime' : endDatetime,
'notes' : modalHandle.find('#notes').val()
};
var customerData = {
'id' : modalHandle.find('#customer-id').val(),
'first_name' : modalHandle.find('#first-name').val(),
'last_name' : modalHandle.find('#last-name').val(),
'email' : modalHandle.find('#email').val(),
'phone_number' : modalHandle.find('#phone-number').val(),
'address' : modalHandle.find('#address').val(),
'city' : modalHandle.find('#city').val(),
'zip_code' : modalHandle.find('#zip-code').val()
};
// :: DEFINE SUCCESS EVENT CALLBACK
var successCallback = function(response) {
if (response.error) {
// There was something wrong within the ajax function.
modalHandle.find('.modal-header').append(
'<br><div class="alert alert-error">' +
response.error +
'</div>');
return;
}
// Display success message to the user.
modalHandle.find('.modal-header').append(
'<br><div class="alert alert-success">' +
'Appointment saved successfully!' +
'</div>');
// Close the modal dialog and refresh the calendar appointments
// after one second.
setTimeout(function() {
modalHandle.find('.alert').remove();
modalHandle.modal('hide');
$('#select-filter-item').trigger('change');
}, 2000);
};
// :: DEFINE ERROR EVENT CALLBACK
var errorCallback = function() {
// Display error message to the user.
modalHandle.find('.modal-header').append(
'<br><div class="alert alert-error">' +
'A server communication error occured, please try again.' +
'</div>');
};
// :: CALL THE UPDATE APPOINTMENT METHOD // :: CALL THE UPDATE APPOINTMENT METHOD
BackendCalendar.updateAppointment(appointmentData); BackendCalendar.updateAppointmentData(appointmentData, customerData,
successCallback, errorCallback);
});
/**
* Event: Enable Synchronization Button "Click"
*
* When the user clicks on the "Enable Sync" button, a popup should appear
* that is going to follow the web server authorization flow of OAuth.
*
* @task Check whether the selected provider has already enabled the sync
* or not.
*/
$('#enable-sync').click(function() {
var authUrl = GlobalVariables.baseUrl + 'google/oauth/'
+ $('#select-filter-item').val();
var redirectUrl = GlobalVariables.baseUrl + 'google/oauth_callback';
var windowHandle = window.open(authUrl, 'Authorize Easy!Appointments',
'width=800, height=600');
var authInterval = window.setInterval(function() {
if (windowHandle.document.URL.indexOf(redirectUrl) !== -1) {
// The user has granted access to his data.
windowHandle.close();
window.clearInterval(authInterval);
$('#enable-sync').addClass('btn-success');
}
}, 100);
}); });
}, },
@ -265,7 +322,7 @@ var BackendCalendar = {
* *
* @return {int} Returns the calendar element height in pixels. * @return {int} Returns the calendar element height in pixels.
*/ */
getCalendarHeight: function () { getCalendarHeight : function () {
var result = window.innerHeight - $('#footer').height() - $('#header').height() var result = window.innerHeight - $('#footer').height() - $('#header').height()
- $('#calendar-toolbar').height() - 80; // 80 for fine tuning - $('#calendar-toolbar').height() - 80; // 80 for fine tuning
return (result > 500) ? result : 500; // Minimum height is 500px return (result > 500) ? result : 500; // Minimum height is 500px
@ -282,9 +339,9 @@ var BackendCalendar = {
* @param {date} startDate Visible start date of the calendar. * @param {date} startDate Visible start date of the calendar.
* @param {type} endDate Visible end date of the calendar. * @param {type} endDate Visible end date of the calendar.
*/ */
refreshCalendarAppointments: function(calendarHandle, recordId, filterType, refreshCalendarAppointments : function(calendarHandle, recordId, filterType,
startDate, endDate) { startDate, endDate) {
var ajaxUrl = GlobalVariables.baseUrl + 'backend/ajax_get_calendar_appointments'; var postUrl = GlobalVariables.baseUrl + 'backend/ajax_get_calendar_appointments';
var postData = { var postData = {
record_id : recordId, record_id : recordId,
@ -293,7 +350,7 @@ var BackendCalendar = {
filter_type : filterType filter_type : filterType
}; };
$.post(ajaxUrl, postData, function(response) { $.post(postUrl, postData, function(response) {
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
//console.log('Refresh Calendar Appointments Response :', response); //console.log('Refresh Calendar Appointments Response :', response);
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
@ -327,8 +384,295 @@ var BackendCalendar = {
* @param {object} appointmentData Contain the new appointment data. The * @param {object} appointmentData Contain the new appointment data. The
* id of the appointment MUST be already included. The rest values must * id of the appointment MUST be already included. The rest values must
* follow the database structure. * follow the database structure.
* @param {object} customerData (OPTIONAL) contains the customer data.
* @param {function} successCallback (OPTIONAL) If defined, this function is
* going to be executed on post success.
* @param {function} errorCallback (OPTIONAL) If defined, this function is
* going to be executed on post failure.
*/ */
updateAppointment: function(appointmentData) { updateAppointmentData : function(appointmentData, customerData,
// @task Save the appointment changes (ajax call). successCallback, errorCallback) {
// :: MAKE AN AJAX CALL TO SERVER - STORE APPOINTMENT DATA
var postUrl = GlobalVariables.baseUrl + 'backend/ajax_save_appointment_changes';
var postData = {};
postData['appointment_data'] = JSON.stringify(appointmentData);
if (customerData !== undefined) {
postData['customer_data'] = JSON.stringify(customerData);
}
$.ajax({
type : 'POST',
url : postUrl,
data : postData,
dataType : 'json',
success : function(response) {
/////////////////////////////////////////////////////////////
console.log('Update Appointment Data Response:', response);
/////////////////////////////////////////////////////////////
if (successCallback !== undefined) {
successCallback(response);
}
},
error : function(jqXHR, textStatus, errorThrown) {
//////////////////////////////////////////////////////////////////
//console.log('Update Appointment Data Error:', jqXHR, textStatus,
// errorThrown);
//////////////////////////////////////////////////////////////////
if (errorCallback !== undefined) {
errorCallback();
}
}
});
},
/**
* Calendar Event "Resize" Callback
*
* The user can change the duration of an event by resizing an appointment
* object on the calendar. This change needs to be stored to the database
* too and this is done via an ajax call.
*
* @see updateAppointmentData()
*/
calendarEventResize : function(event, dayDelta, minuteDelta, revertFunc,
jsEvent, ui, view) {
// :: PREPARE THE APPOINTMENT DATA
var appointmentData = GeneralFunctions.clone(event.data);
// Must delete the following because only appointment data should be
// provided to the ajax call.
delete appointmentData['customer'];
delete appointmentData['provider'];
delete appointmentData['service'];
appointmentData['end_datetime'] = Date.parseExact(
appointmentData['end_datetime'], 'yyyy-MM-dd HH:mm:ss')
.add({ minutes: minuteDelta })
.toString('yyyy-MM-dd HH:mm:ss');
// :: DEFINE THE SUCCESS CALLBACK FUNCTION
var successCallback = function(response) {
if (response.error) {
// Display error message to the user.
Backend.displayNotification(reponse.error);
return;
}
// Display success notification to user.
var undoFunction = function() {
appointmentData['end_datetime'] = Date.parseExact(
appointmentData['end_datetime'], 'yyyy-MM-dd HH:mm:ss')
.add({ minutes: -minuteDelta })
.toString('yyyy-MM-dd HH:mm:ss');
var postUrl = GlobalVariables.baseUrl
+ 'backend/ajax_save_appointment_changes';
var postData = {
'appointment_data' : JSON.stringify(appointmentData)
};
$.post(postUrl, postData, function(response) {
$('#notification').hide('blind');
revertFunc();
});
};
Backend.displayNotification('Appointment updated successfully!', [
{
'label' : 'Undo',
'function' : undoFunction
}
]);
$('#footer').css('position', 'static'); // Footer position fix.
};
// :: UPDATE APPOINTMENT DATA VIA AJAX CALL
BackendCalendar.updateAppointmentData(appointmentData, undefined,
successCallback, undefined);
},
/**
* Calendar Window "Resize" Callback
*
* The calendar element needs to be resized too in order to fit into the
* window. Nevertheless, if the window becomes very small the the calendar
* won't shrink anymore.
*
* @see getCalendarHeight()
*/
calendarWindowResize : function(view) {
$('#calendar').fullCalendar('option', 'height',
BackendCalendar.getCalendarHeight());
},
/**
* Calendar Day "Click" Callback
*
* When the user clicks on a day square on the calendar, then he will
* automatically be transfered to that day view calendar.
*/
calendarDayClick : function(date, allDay, jsEvent, view) {
if (allDay) {
// Switch to day view
$('#calendar').fullCalendar('gotoDate', date);
$('#calendar').fullCalendar('changeView', 'agendaDay');
}
},
/**
* Calendar Event "Click" Callback
*
* When the user clicks on an appointment object on the calendar, then
* a data preview popover is display above the calendar item.
*/
calendarEventClick : function(event, jsEvent, view) {
// Display a popover with the event details.
var html =
'<style type="text/css">'
+ '.popover-content strong {min-width: 80px; display:inline-block;}'
+ '.popover-content button {margin-right: 10px;}'
+ '</style>' +
'<strong>Start</strong> '
+ event.start.toString('dd/MM/yyyy HH:mm')
+ '<br>' +
'<strong>End</strong> '
+ event.end.toString('dd/MM/yyyy HH:mm')
+ '<br>' +
'<strong>Service</strong> '
+ event.title
+ '<br>' +
'<strong>Provider</strong> '
+ event.data['provider']['first_name'] + ' '
+ event.data['provider']['last_name']
+ '<br>' +
'<strong>Customer</strong> '
+ event.data['customer']['first_name'] + ' '
+ event.data['customer']['last_name']
+ '<hr>' +
'<center>' +
'<button class="edit-popover btn btn-primary">Edit</button>' +
'<button class="close-popover btn" data-po=' + jsEvent.target + '>Close</button>' +
'</center>';
$(jsEvent.target).popover({
placement : 'top',
title : event.title,
content : html,
html : true,
container : 'body',
trigger : 'manual'
});
BackendCalendar.lastFocusedEventData = event;
$(jsEvent.target).popover('show');
},
/**
* Calendar Event "Drop" Callback
*
* This event handler is triggered whenever the user drags and drops
* an event into a different position on the calendar. We need to update
* the database with this change. This is done via an ajax call.
*/
calendarEventDrop : function(event, dayDelta, minuteDelta, allDay,
revertFunc, jsEvent, ui, view) {
// :: PREPARE THE APPOINTMENT DATA
var appointmentData = GeneralFunctions.clone(event.data);
// Must delete the following because only appointment data should be
// provided to the ajax call.
delete appointmentData['customer'];
delete appointmentData['provider'];
delete appointmentData['service'];
appointmentData['start_datetime'] = Date.parseExact(
appointmentData['start_datetime'], 'yyyy-MM-dd HH:mm:ss')
.add({ days: dayDelta, minutes: minuteDelta })
.toString('yyyy-MM-dd HH:mm:ss');
appointmentData['end_datetime'] = Date.parseExact(
appointmentData['end_datetime'], 'yyyy-MM-dd HH:mm:ss')
.add({ days: dayDelta, minutes: minuteDelta })
.toString('yyyy-MM-dd HH:mm:ss');
event.data['start_datetime'] = appointmentData['start_datetime'];
event.data['end_datetime'] = appointmentData['end_datetime'];
// :: DEFINE THE SUCCESS CALLBACK FUNCTION
var successCallback = function(response) {
if (response.error) {
// Display error message to the user.
Backend.displayNotification(reponse.error);
return;
}
// Display success notification to user.
var undoFunction = function() {
appointmentData['start_datetime'] = Date.parseExact(
appointmentData['start_datetime'], 'yyyy-MM-dd HH:mm:ss')
.add({ days: -dayDelta, minutes: -minuteDelta })
.toString('yyyy-MM-dd HH:mm:ss');
appointmentData['end_datetime'] = Date.parseExact(
appointmentData['end_datetime'], 'yyyy-MM-dd HH:mm:ss')
.add({ days: -dayDelta, minutes: -minuteDelta })
.toString('yyyy-MM-dd HH:mm:ss');
event.data['start_datetime'] = appointmentData['start_datetime'];
event.data['end_datetime'] = appointmentData['end_datetime'];
var postUrl = GlobalVariables.baseUrl
+ 'backend/ajax_save_appointment_changes';
var postData = {
'appointment_data' : JSON.stringify(appointmentData)
};
$.post(postUrl, postData, function(response) {
$('#notification').hide('blind');
revertFunc();
});
};
Backend.displayNotification('Appointment updated successfully!', [
{
'label' : 'Undo',
'function' : undoFunction
}
]);
$('#footer').css('position', 'static'); // Footer position fix.
};
// :: UPDATE APPOINTMENT DATA VIA AJAX CALL
BackendCalendar.updateAppointmentData(appointmentData, undefined,
successCallback, undefined);
},
/**
* Calendar "View Display" Callback
*
* Whenever the calendar changes or refreshes its view certain actions
* need to be made, in order to display proper information to the user.
*/
calendarViewDisplay : function(view) {
// Place the footer into correct position because the calendar
// height might change.
BackendCalendar.refreshCalendarAppointments(
$('#calendar'),
$('#select-filter-item').val(),
$('#select-filter-item option:selected').attr('type'),
$('#calendar').fullCalendar('getView').visStart,
$('#calendar').fullCalendar('getView').visEnd);
$(window).trigger('resize');
$('.fv-events').each(function(index, eventHandle) {
$(eventHandle).popover();
});
} }
}; };

View file

@ -119,5 +119,49 @@ var GeneralFunctions = {
+ pad(dt.getUTCHours())+':' + pad(dt.getUTCHours())+':'
+ pad(dt.getUTCMinutes())+':' + pad(dt.getUTCMinutes())+':'
+ pad(dt.getUTCSeconds())+'Z' + pad(dt.getUTCSeconds())+'Z'
},
/**
* This method creates and returns an exact copy of the provided object.
* It is very usefull whenever changes need to be made to an object without
* modyfing the original data.
*
* @link http://stackoverflow.com/questions/728360/most-elegant-way-to-clone-a-javascript-object
*
* @param {object} originalObject Object to be copied.
* @returns {object} Returns an exact copy of the provided element.
*/
clone: function(originalObject) {
// Handle the 3 simple types, and null or undefined
if (null == originalObject || "object" != typeof originalObject)
return originalObject;
// Handle Date
if (originalObject instanceof Date) {
var copy = new Date();
copy.setTime(originalObject.getTime());
return copy;
}
// Handle Array
if (originalObject instanceof Array) {
var copy = [];
for (var i = 0, len = originalObject.length; i < len; i++) {
copy[i] = GeneralFunctions.clone(originalObject[i]);
}
return copy;
}
// Handle Object
if (originalObject instanceof Object) {
var copy = {};
for (var attr in originalObject) {
if (originalObject.hasOwnProperty(attr))
copy[attr] = GeneralFunctions.clone(originalObject[attr]);
}
return copy;
}
throw new Error("Unable to copy obj! Its type isn't supported.");
} }
} };

File diff suppressed because it is too large Load diff