From fc53817e817ecb733ba14f0426467d4f34bf7a6a Mon Sep 17 00:00:00 2001 From: "alextselegidis@gmail.com" Date: Tue, 18 Jun 2013 16:06:34 +0000 Subject: [PATCH] =?UTF-8?q?=CE=9F=CE=BB=CE=BF=CE=BA=CE=BB=CE=AE=CF=81?= =?UTF-8?q?=CF=89=CF=83=CE=B7=20=CF=84=CE=BF=CF=85=20=CF=80=CF=81=CF=8E?= =?UTF-8?q?=CF=84=CE=BF=CF=85=20=CE=BC=CE=AD=CF=81=CE=BF=CF=85=CF=82=20?= =?UTF-8?q?=CE=B4=CF=85=CE=BD=CE=B1=CF=84=CE=BF=CF=84=CE=AE=CF=84=CF=89?= =?UTF-8?q?=CE=BD=20=CF=84=CE=B7=CF=82=20=CF=83=CE=B5=CE=BB=CE=AF=CE=B4?= =?UTF-8?q?=CE=B1=CF=82=20Calendar=20=CF=84=CE=BF=CF=85=20backend.=20?= =?UTF-8?q?=CE=A3=CF=87=CE=B5=CE=B4=CE=AF=CE=B1=CF=83=CE=B7=20=CE=BA=CE=B1?= =?UTF-8?q?=CE=B9=20=CF=80=CF=81=CE=BF=CE=B5=CF=84=CE=BF=CE=B9=CE=BC=CE=B1?= =?UTF-8?q?=CF=83=CE=AF=CE=B1=20=CF=84=CE=BF=CF=85=20=CF=84=CF=81=CF=8C?= =?UTF-8?q?=CF=80=CE=BF=CF=85=20=CE=BC=CE=B5=20=CF=84=CE=BF=CE=BD=20=CE=BF?= =?UTF-8?q?=CF=80=CE=BF=CE=AF=CE=BF=20=CE=B8=CE=B1=20=CE=B5=CE=BA=CF=84?= =?UTF-8?q?=CE=B5=CE=BB=CE=B5=CE=AF=CF=84=CE=B1=CE=B9=20=CE=B7=20=CE=B4?= =?UTF-8?q?=CE=B9=CE=B1=CE=B4=CE=B9=CE=BA=CE=B1=CF=83=CE=AF=CE=B1=20OAuth,?= =?UTF-8?q?=20=CE=AD=CF=84=CF=83=CE=B9=20=CF=8E=CF=83=CF=84=CE=B5=20=CE=BD?= =?UTF-8?q?=CE=B1=20=CF=83=CF=85=CF=87=CF=81=CE=BF=CE=BD=CE=AF=CE=B6=CE=BF?= =?UTF-8?q?=CE=BD=CF=84=CE=B1=CE=B9=20=CF=84=CE=B1=20=CF=80=CE=BB=CE=AC?= =?UTF-8?q?=CE=BD=CE=B1=20=CF=84=CF=89=CE=BD=20=CF=80=CE=AC=CF=81=CE=BF?= =?UTF-8?q?=CF=87=CF=89=CE=BD=20=CE=BC=CE=B5=20=CF=84=CE=BF=20Google=20Cal?= =?UTF-8?q?endar.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Release Notes.txt | 22 +- install.sh | 36 - src/application/controllers/backend.php | 46 +- src/application/controllers/google.php | 40 + src/application/views/backend/calendar.php | 26 +- src/application/views/backend/customers.php | 7 + src/application/views/backend/header.php | 7 +- src/application/views/backend/providers.php | 7 + src/application/views/backend/services.php | 7 + src/application/views/backend/settings.php | 7 + src/assets/css/backend.css | 4 +- src/assets/css/general.css | 12 + src/assets/js/backend.js | 40 +- src/assets/js/backend_calendar.js | 544 ++++- src/assets/js/general_functions.js | 46 +- .../libs/jquery/jquery-ui-timepicker-addon.js | 2103 +++++++++++++++++ 16 files changed, 2797 insertions(+), 157 deletions(-) delete mode 100644 install.sh create mode 100644 src/application/controllers/google.php create mode 100644 src/application/views/backend/customers.php create mode 100644 src/application/views/backend/providers.php create mode 100644 src/application/views/backend/services.php create mode 100644 src/application/views/backend/settings.php create mode 100644 src/assets/css/general.css create mode 100644 src/assets/js/libs/jquery/jquery-ui-timepicker-addon.js diff --git a/Release Notes.txt b/Release Notes.txt index a3386a31..090bc6d2 100644 --- a/Release Notes.txt +++ b/Release Notes.txt @@ -1,9 +1,15 @@ -VERSION 0.2 +VERSION 0.3 =========== -- Use the PHPMailer class for sending HTML emails. -- Display error message to users. -- Includes complete Google Sync protocol document. -- Customers can book appointments only for the available hours. -- Generation of code documentation. -- Customers can edit the application through unique links (from their emails). -- Minor Fixes +Main + +- First backend-calendar page implementation (not complete, admin's perspective). +- Javascript Google API usage from the customer's perspective. +- Backend google calendar authentication Process. +- Sync every appointment change made from E!A to Google Calendar. +- Display user friendly error messages. + + +Minor + +- Added sync exception to Google Sync library. + \ No newline at end of file diff --git a/install.sh b/install.sh deleted file mode 100644 index 1bd4acbb..00000000 --- a/install.sh +++ /dev/null @@ -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 diff --git a/src/application/controllers/backend.php b/src/application/controllers/backend.php index c6bad6a2..ac5ecdca 100644 --- a/src/application/controllers/backend.php +++ b/src/application/controllers/backend.php @@ -28,19 +28,19 @@ class Backend extends CI_Controller { } public function customers() { - + echo '

Not implemented yet.

'; } public function services() { - + echo '

Not implemented yet.

'; } public function providers() { - + echo '

Not implemented yet.

'; } public function settings() { - + echo '

Not implemented yet.

'; } /** @@ -50,8 +50,8 @@ class Backend extends CI_Controller { * period and record type (provider or service). * * @param {numeric} $_POST['record_id'] Selected record id. - * @param {string} $_POST['filter_type'] Could be either FILTER_TYPE_PROVIDER or - * FILTER_TYPE_SERVICE. + * @param {string} $_POST['filter_type'] Could be either FILTER_TYPE_PROVIDER + * or FILTER_TYPE_SERVICE. * @param {string} $_POST['start_date'] The user selected start date. * @param {string} $_POST['end_date'] The user selected end date. */ @@ -86,6 +86,40 @@ class Backend extends CI_Controller { 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 */ diff --git a/src/application/controllers/google.php b/src/application/controllers/google.php new file mode 100644 index 00000000..c0abd7e3 --- /dev/null +++ b/src/application/controllers/google.php @@ -0,0 +1,40 @@ +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. + * + * IMPORTANT! 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 */ \ No newline at end of file diff --git a/src/application/views/backend/calendar.php b/src/application/views/backend/calendar.php index f20f89ec..5d6f5993 100644 --- a/src/application/views/backend/calendar.php +++ b/src/application/views/backend/calendar.php @@ -3,6 +3,9 @@ + + @@ -39,9 +42,15 @@ Enable Sync + + @@ -62,6 +71,8 @@
Appointment Details + +
@@ -77,9 +88,16 @@
- +
- + +
+
+ +
+ +
+
@@ -87,6 +105,8 @@
Customer Details + +
diff --git a/src/application/views/backend/customers.php b/src/application/views/backend/customers.php new file mode 100644 index 00000000..b6eddc13 --- /dev/null +++ b/src/application/views/backend/customers.php @@ -0,0 +1,7 @@ + diff --git a/src/application/views/backend/header.php b/src/application/views/backend/header.php index 24efacb2..406288fd 100644 --- a/src/application/views/backend/header.php +++ b/src/application/views/backend/header.php @@ -30,6 +30,10 @@ rel="stylesheet" type="text/css" href="assets/css/backend.css"> +
-
\ No newline at end of file + + \ No newline at end of file diff --git a/src/application/views/backend/providers.php b/src/application/views/backend/providers.php new file mode 100644 index 00000000..b6eddc13 --- /dev/null +++ b/src/application/views/backend/providers.php @@ -0,0 +1,7 @@ + diff --git a/src/application/views/backend/services.php b/src/application/views/backend/services.php new file mode 100644 index 00000000..b6eddc13 --- /dev/null +++ b/src/application/views/backend/services.php @@ -0,0 +1,7 @@ + diff --git a/src/application/views/backend/settings.php b/src/application/views/backend/settings.php new file mode 100644 index 00000000..b6eddc13 --- /dev/null +++ b/src/application/views/backend/settings.php @@ -0,0 +1,7 @@ + diff --git a/src/assets/css/backend.css b/src/assets/css/backend.css index 8d723f2b..2447f891 100644 --- a/src/assets/css/backend.css +++ b/src/assets/css/backend.css @@ -30,6 +30,8 @@ root { } #footer #footer-content { padding: 15px; } +#notification strong { margin-right: 15px; } + /* BACKEND CALENDAR PAGE -------------------------------------------------------------------- */ #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; font-weight: bold; font-size: 18px; } #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; } /* BACKEND CUSTOMERS PAGE diff --git a/src/assets/css/general.css b/src/assets/css/general.css new file mode 100644 index 00000000..3a532a35 --- /dev/null +++ b/src/assets/css/general.css @@ -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; } \ No newline at end of file diff --git a/src/assets/js/backend.js b/src/assets/js/backend.js index adabb5e6..43cde7a3 100644 --- a/src/assets/js/backend.js +++ b/src/assets/js/backend.js @@ -32,5 +32,43 @@ var Backend = { '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 = + '
' + + '' + message + ''; + + $.each(actions, function(index, action) { + var actionId = action['label'].toLowerCase().replace(' ', '-'); + notificationHtml += ''; + + $(document).off('click', '#' + actionId); + $(document).on('click', '#' + actionId, action['function']); + }); + + notificationHtml += '
'; + + var leftValue = window.innerWidth / 2 - $('body .notification').width() - 200; + + $('#notification').html(notificationHtml); + $('#notification').show('blind'); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/assets/js/backend_calendar.js b/src/assets/js/backend_calendar.js index f8428323..093591b6 100644 --- a/src/assets/js/backend_calendar.js +++ b/src/assets/js/backend_calendar.js @@ -4,10 +4,12 @@ * @namespace BackendCalendar */ var BackendCalendar = { + // :: NAMESPACE CONSTANTS FILTER_TYPE_PROVIDER : 'provider', 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 @@ -17,7 +19,7 @@ var BackendCalendar = { * @param {bool} defaultEventHandlers (OPTIONAL = TRUE) Determines whether the * default event handlers will be set for the current page. */ - initialize: function(defaultEventHandlers) { + initialize : function(defaultEventHandlers) { if (defaultEventHandlers === undefined) defaultEventHandlers = true; // :: INITIALIZE THE DOM ELEMENTS OF THE PAGE @@ -25,6 +27,7 @@ var BackendCalendar = { defaultView : 'agendaWeek', height : BackendCalendar.getCalendarHeight(), editable : true, + slotMinutes : 15, columnFormat : { month : 'ddd', week : 'ddd d/M', @@ -40,84 +43,20 @@ var BackendCalendar = { center : 'title', 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 = - '' + - 'Start ' - + event.start.toString('dd-MM-yyyy HH:mm') - + '
' + - 'End ' - + event.end.toString('dd-MM-yyyy HH:mm') - + '
' + - 'Service ' - + event.title - + '
' + - 'Provider ' - + event.data['provider']['first_name'] + ' ' - + event.data['provider']['last_name'] - + '
' + - 'Customer ' - + event.data['customer']['first_name'] + ' ' - + event.data['customer']['last_name'] - + '
' + - '
' + - '' + - '' + - '
'; - - $(jsEvent.target).popover({ - placement : 'top', - title : event.title, - content : html, - html : true, - container : 'body', - trigger : 'manual' - }); - - $(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(); - }); - } + + // Calendar events need to be declared on initialization. + windowResize : BackendCalendar.calendarWindowResize, + viewDisplay : BackendCalendar.calendarViewDisplay, + dayClick : BackendCalendar.calendarDayClick, + eventClick : BackendCalendar.calendarEventClick, + eventResize : BackendCalendar.calendarEventResize, + eventDrop : BackendCalendar.calendarEventDrop }); + // Trigger once to set the proper footer position after calendar + // initialization. + BackendCalendar.calendarWindowResize(); + // :: FILL THE SELECT ELEMENTS OF THE PAGE var optgroupHtml = ''; $.each(GlobalVariables.availableProviders, function(index, provider) { @@ -131,8 +70,8 @@ var BackendCalendar = { optgroupHtml = ''; $.each(GlobalVariables.availableServices, function(index, service) { optgroupHtml += ''; + 'type="' + BackendCalendar.FILTER_TYPE_SERVICE + '">' + + service['name'] + ''; }); 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 * 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 * display them on the calendar. @@ -164,11 +103,20 @@ var BackendCalendar = { $('#calendar').fullCalendar('getView').visStart, $('#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. */ @@ -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. */ $(document).on('click', '.edit-popover', function() { $(this).parents().eq(2).remove(); // Hide the popover - var appointmentData = BackendCalendar.lastFocusedEvent.data; + var appointmentData = BackendCalendar.lastFocusedEventData.data; var modalHandle = $('#manage-appointment'); // :: APPLY APPOINTMENT DATA AND SHOW TO MODAL DIALOG modalHandle.find('input, textarea').val(''); + modalHandle.find('#appointment-id').val(appointmentData['id']); // Fill the services listbox and select the appointment service. $.each(GlobalVariables.availableServices, function(index, service) { @@ -220,7 +169,25 @@ var BackendCalendar = { 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']; + modalHandle.find('#customer-id').val(appointmentData['id_users_customer']); modalHandle.find('#first-name').val(customerData['first_name']); modalHandle.find('#last-name').val(customerData['last_name']); modalHandle.find('#email').val(customerData['email']); @@ -228,16 +195,14 @@ var BackendCalendar = { modalHandle.find('#address').val(customerData['address']); modalHandle.find('#city').val(customerData['city']); 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 $('#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. */ @@ -246,17 +211,109 @@ var BackendCalendar = { }); /** - * Event: Manage Appointments Dialog Save Button "Clicked" + * Event: Manage Appointments Dialog Save Button "Click" * * Stores the appointment changes. */ $('#manage-appointment #save-button').click(function() { // :: 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( + '
' + + response.error + + '
'); + return; + } + + // Display success message to the user. + modalHandle.find('.modal-header').append( + '
' + + 'Appointment saved successfully!' + + '
'); + + // 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( + '
' + + 'A server communication error occured, please try again.' + + '
'); + }; // :: 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. */ - getCalendarHeight: function () { + getCalendarHeight : function () { var result = window.innerHeight - $('#footer').height() - $('#header').height() - $('#calendar-toolbar').height() - 80; // 80 for fine tuning 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 {type} endDate Visible end date of the calendar. */ - refreshCalendarAppointments: function(calendarHandle, recordId, filterType, + refreshCalendarAppointments : function(calendarHandle, recordId, filterType, startDate, endDate) { - var ajaxUrl = GlobalVariables.baseUrl + 'backend/ajax_get_calendar_appointments'; + var postUrl = GlobalVariables.baseUrl + 'backend/ajax_get_calendar_appointments'; var postData = { record_id : recordId, @@ -293,7 +350,7 @@ var BackendCalendar = { filter_type : filterType }; - $.post(ajaxUrl, postData, function(response) { + $.post(postUrl, postData, function(response) { //////////////////////////////////////////////////////////////////// //console.log('Refresh Calendar Appointments Response :', response); //////////////////////////////////////////////////////////////////// @@ -327,8 +384,295 @@ var BackendCalendar = { * @param {object} appointmentData Contain the new appointment data. The * id of the appointment MUST be already included. The rest values must * 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) { - // @task Save the appointment changes (ajax call). + updateAppointmentData : function(appointmentData, customerData, + 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 = + '' + + 'Start ' + + event.start.toString('dd/MM/yyyy HH:mm') + + '
' + + 'End ' + + event.end.toString('dd/MM/yyyy HH:mm') + + '
' + + 'Service ' + + event.title + + '
' + + 'Provider ' + + event.data['provider']['first_name'] + ' ' + + event.data['provider']['last_name'] + + '
' + + 'Customer ' + + event.data['customer']['first_name'] + ' ' + + event.data['customer']['last_name'] + + '
' + + '
' + + '' + + '' + + '
'; + + $(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(); + }); } }; \ No newline at end of file diff --git a/src/assets/js/general_functions.js b/src/assets/js/general_functions.js index d08f3143..aef1ac5e 100644 --- a/src/assets/js/general_functions.js +++ b/src/assets/js/general_functions.js @@ -119,5 +119,49 @@ var GeneralFunctions = { + pad(dt.getUTCHours())+':' + pad(dt.getUTCMinutes())+':' + 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."); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/assets/js/libs/jquery/jquery-ui-timepicker-addon.js b/src/assets/js/libs/jquery/jquery-ui-timepicker-addon.js new file mode 100644 index 00000000..fc17c6cd --- /dev/null +++ b/src/assets/js/libs/jquery/jquery-ui-timepicker-addon.js @@ -0,0 +1,2103 @@ +/* + * jQuery timepicker addon + * By: Trent Richardson [http://trentrichardson.com] + * Version 1.3 + * Last Modified: 05/05/2013 + * + * Copyright 2013 Trent Richardson + * You may use this project under MIT or GPL licenses. + * http://trentrichardson.com/Impromptu/GPL-LICENSE.txt + * http://trentrichardson.com/Impromptu/MIT-LICENSE.txt + */ + +/*jslint evil: true, white: false, undef: false, nomen: false */ + +(function($) { + + /* + * Lets not redefine timepicker, Prevent "Uncaught RangeError: Maximum call stack size exceeded" + */ + $.ui.timepicker = $.ui.timepicker || {}; + if ($.ui.timepicker.version) { + return; + } + + /* + * Extend jQueryUI, get it started with our version number + */ + $.extend($.ui, { + timepicker: { + version: "1.3" + } + }); + + /* + * Timepicker manager. + * Use the singleton instance of this class, $.timepicker, to interact with the time picker. + * Settings for (groups of) time pickers are maintained in an instance object, + * allowing multiple different settings on the same page. + */ + var Timepicker = function() { + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + currentText: 'Now', + closeText: 'Done', + amNames: ['AM', 'A'], + pmNames: ['PM', 'P'], + timeFormat: 'HH:mm', + timeSuffix: '', + timeOnlyTitle: 'Choose Time', + timeText: 'Time', + hourText: 'Hour', + minuteText: 'Minute', + secondText: 'Second', + millisecText: 'Millisecond', + microsecText: 'Microsecond', + timezoneText: 'Time Zone', + isRTL: false + }; + this._defaults = { // Global defaults for all the datetime picker instances + showButtonPanel: true, + timeOnly: false, + showHour: null, + showMinute: null, + showSecond: null, + showMillisec: null, + showMicrosec: null, + showTimezone: null, + showTime: true, + stepHour: 1, + stepMinute: 1, + stepSecond: 1, + stepMillisec: 1, + stepMicrosec: 1, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null, + hourMin: 0, + minuteMin: 0, + secondMin: 0, + millisecMin: 0, + microsecMin: 0, + hourMax: 23, + minuteMax: 59, + secondMax: 59, + millisecMax: 999, + microsecMax: 999, + minDateTime: null, + maxDateTime: null, + onSelect: null, + hourGrid: 0, + minuteGrid: 0, + secondGrid: 0, + millisecGrid: 0, + microsecGrid: 0, + alwaysSetTime: true, + separator: ' ', + altFieldTimeOnly: true, + altTimeFormat: null, + altSeparator: null, + altTimeSuffix: null, + pickerTimeFormat: null, + pickerTimeSuffix: null, + showTimepicker: true, + timezoneList: null, + addSliderAccess: false, + sliderAccessArgs: null, + controlType: 'slider', + defaultValue: null, + parse: 'strict' + }; + $.extend(this._defaults, this.regional['']); + }; + + $.extend(Timepicker.prototype, { + $input: null, + $altInput: null, + $timeObj: null, + inst: null, + hour_slider: null, + minute_slider: null, + second_slider: null, + millisec_slider: null, + microsec_slider: null, + timezone_select: null, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null, + hourMinOriginal: null, + minuteMinOriginal: null, + secondMinOriginal: null, + millisecMinOriginal: null, + microsecMinOriginal: null, + hourMaxOriginal: null, + minuteMaxOriginal: null, + secondMaxOriginal: null, + millisecMaxOriginal: null, + microsecMaxOriginal: null, + ampm: '', + formattedDate: '', + formattedTime: '', + formattedDateTime: '', + timezoneList: null, + units: ['hour','minute','second','millisec', 'microsec'], + support: {}, + control: null, + + /* + * Override the default settings for all instances of the time picker. + * @param settings object - the new settings to use as defaults (anonymous object) + * @return the manager object + */ + setDefaults: function(settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* + * Create a new Timepicker instance + */ + _newInst: function($input, o) { + var tp_inst = new Timepicker(), + inlineSettings = {}, + fns = {}, + overrides, i; + + for (var attrName in this._defaults) { + if(this._defaults.hasOwnProperty(attrName)){ + var attrValue = $input.attr('time:' + attrName); + if (attrValue) { + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + } + + overrides = { + beforeShow: function (input, dp_inst) { + if ($.isFunction(tp_inst._defaults.evnts.beforeShow)) { + return tp_inst._defaults.evnts.beforeShow.call($input[0], input, dp_inst, tp_inst); + } + }, + onChangeMonthYear: function (year, month, dp_inst) { + // Update the time as well : this prevents the time from disappearing from the $input field. + tp_inst._updateDateTime(dp_inst); + if ($.isFunction(tp_inst._defaults.evnts.onChangeMonthYear)) { + tp_inst._defaults.evnts.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst); + } + }, + onClose: function (dateText, dp_inst) { + if (tp_inst.timeDefined === true && $input.val() !== '') { + tp_inst._updateDateTime(dp_inst); + } + if ($.isFunction(tp_inst._defaults.evnts.onClose)) { + tp_inst._defaults.evnts.onClose.call($input[0], dateText, dp_inst, tp_inst); + } + } + }; + for (i in overrides) { + if (overrides.hasOwnProperty(i)) { + fns[i] = o[i] || null; + } + } + + tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, o, overrides, { + evnts:fns, + timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker'); + }); + tp_inst.amNames = $.map(tp_inst._defaults.amNames, function(val) { + return val.toUpperCase(); + }); + tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function(val) { + return val.toUpperCase(); + }); + + // detect which units are supported + tp_inst.support = detectSupport( + tp_inst._defaults.timeFormat + + (tp_inst._defaults.pickerTimeFormat? tp_inst._defaults.pickerTimeFormat:'') + + (tp_inst._defaults.altTimeFormat? tp_inst._defaults.altTimeFormat:'')); + + // controlType is string - key to our this._controls + if(typeof(tp_inst._defaults.controlType) === 'string'){ + if(tp_inst._defaults.controlType == 'slider' && typeof(jQuery.ui.slider) === 'undefined'){ + tp_inst._defaults.controlType = 'select'; + } + tp_inst.control = tp_inst._controls[tp_inst._defaults.controlType]; + } + // controlType is an object and must implement create, options, value methods + else{ + tp_inst.control = tp_inst._defaults.controlType; + } + + // prep the timezone options + var timezoneList = [-720,-660,-600,-570,-540,-480,-420,-360,-300,-270,-240,-210,-180,-120,-60, + 0,60,120,180,210,240,270,300,330,345,360,390,420,480,525,540,570,600,630,660,690,720,765,780,840]; + if (tp_inst._defaults.timezoneList !== null) { + timezoneList = tp_inst._defaults.timezoneList; + } + var tzl=timezoneList.length,tzi=0,tzv=null; + if (tzl > 0 && typeof timezoneList[0] !== 'object') { + for(; tzi tp_inst._defaults.hourMax? tp_inst._defaults.hourMax : tp_inst._defaults.hour; + tp_inst.minute = tp_inst._defaults.minute < tp_inst._defaults.minuteMin? tp_inst._defaults.minuteMin : + tp_inst._defaults.minute > tp_inst._defaults.minuteMax? tp_inst._defaults.minuteMax : tp_inst._defaults.minute; + tp_inst.second = tp_inst._defaults.second < tp_inst._defaults.secondMin? tp_inst._defaults.secondMin : + tp_inst._defaults.second > tp_inst._defaults.secondMax? tp_inst._defaults.secondMax : tp_inst._defaults.second; + tp_inst.millisec = tp_inst._defaults.millisec < tp_inst._defaults.millisecMin? tp_inst._defaults.millisecMin : + tp_inst._defaults.millisec > tp_inst._defaults.millisecMax? tp_inst._defaults.millisecMax : tp_inst._defaults.millisec; + tp_inst.microsec = tp_inst._defaults.microsec < tp_inst._defaults.microsecMin? tp_inst._defaults.microsecMin : + tp_inst._defaults.microsec > tp_inst._defaults.microsecMax? tp_inst._defaults.microsecMax : tp_inst._defaults.microsec; + tp_inst.ampm = ''; + tp_inst.$input = $input; + + if (o.altField) { + tp_inst.$altInput = $(o.altField).css({ + cursor: 'pointer' + }).focus(function() { + $input.trigger("focus"); + }); + } + + if (tp_inst._defaults.minDate === 0 || tp_inst._defaults.minDateTime === 0) { + tp_inst._defaults.minDate = new Date(); + } + if (tp_inst._defaults.maxDate === 0 || tp_inst._defaults.maxDateTime === 0) { + tp_inst._defaults.maxDate = new Date(); + } + + // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime.. + if (tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) { + tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime()); + } + if (tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) { + tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime()); + } + if (tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) { + tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime()); + } + if (tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) { + tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime()); + } + tp_inst.$input.bind('focus', function() { + tp_inst._onFocus(); + }); + + return tp_inst; + }, + + /* + * add our sliders to the calendar + */ + _addTimePicker: function(dp_inst) { + var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ? this.$input.val() + ' ' + this.$altInput.val() : this.$input.val(); + + this.timeDefined = this._parseTime(currDT); + this._limitMinMaxDateTime(dp_inst, false); + this._injectTimePicker(); + }, + + /* + * parse the time string from input value or _setTime + */ + _parseTime: function(timeString, withDate) { + if (!this.inst) { + this.inst = $.datepicker._getInst(this.$input[0]); + } + + if (withDate || !this._defaults.timeOnly) { + var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat'); + try { + var parseRes = parseDateTimeInternal(dp_dateFormat, this._defaults.timeFormat, timeString, $.datepicker._getFormatConfig(this.inst), this._defaults); + if (!parseRes.timeObj) { + return false; + } + $.extend(this, parseRes.timeObj); + } catch (err) { + $.timepicker.log("Error parsing the date/time string: " + err + + "\ndate/time string = " + timeString + + "\ntimeFormat = " + this._defaults.timeFormat + + "\ndateFormat = " + dp_dateFormat); + return false; + } + return true; + } else { + var timeObj = $.datepicker.parseTime(this._defaults.timeFormat, timeString, this._defaults); + if (!timeObj) { + return false; + } + $.extend(this, timeObj); + return true; + } + }, + + /* + * generate and inject html for timepicker into ui datepicker + */ + _injectTimePicker: function() { + var $dp = this.inst.dpDiv, + o = this.inst.settings, + tp_inst = this, + litem = '', + uitem = '', + show = null, + max = {}, + gridSize = {}, + size = null, + i=0, + l=0; + + // Prevent displaying twice + if ($dp.find("div.ui-timepicker-div").length === 0 && o.showTimepicker) { + var noDisplay = ' style="display:none;"', + html = '
' + '
' + o.timeText + '
' + + '
'; + + // Create the markup + for(i=0,l=this.units.length; i' + o[litem +'Text'] + '' + + '
'; + + if (show && o[litem+'Grid'] > 0) { + html += '
'; + + if(litem == 'hour'){ + for (var h = o[litem+'Min']; h <= max[litem]; h += parseInt(o[litem+'Grid'], 10)) { + gridSize[litem]++; + var tmph = $.datepicker.formatTime(this.support.ampm? 'hht':'HH', {hour:h}, o); + html += ''; + } + } + else{ + for (var m = o[litem+'Min']; m <= max[litem]; m += parseInt(o[litem+'Grid'], 10)) { + gridSize[litem]++; + html += ''; + } + } + + html += '
' + tmph + '' + ((m < 10) ? '0' : '') + m + '
'; + } + html += '
'; + } + + // Timezone + var showTz = o.showTimezone !== null? o.showTimezone : this.support.timezone; + html += '
' + o.timezoneText + '
'; + html += '
'; + + // Create the elements from string + html += '
'; + var $tp = $(html); + + // if we only want time picker... + if (o.timeOnly === true) { + $tp.prepend('
' + '
' + o.timeOnlyTitle + '
' + '
'); + $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide(); + } + + // add sliders, adjust grids, add events + for(i=0,l=tp_inst.units.length; i 0) { + size = 100 * gridSize[litem] * o[litem+'Grid'] / (max[litem] - o[litem+'Min']); + $tp.find('.ui_tpicker_'+litem+' table').css({ + width: size + "%", + marginLeft: o.isRTL? '0' : ((size / (-2 * gridSize[litem])) + "%"), + marginRight: o.isRTL? ((size / (-2 * gridSize[litem])) + "%") : '0', + borderCollapse: 'collapse' + }).find("td").click(function(e){ + var $t = $(this), + h = $t.html(), + n = parseInt(h.replace(/[^0-9]/g),10), + ap = h.replace(/[^apm]/ig), + f = $t.data('for'); // loses scope, so we use data-for + + if(f == 'hour'){ + if(ap.indexOf('p') !== -1 && n < 12){ + n += 12; + } + else{ + if(ap.indexOf('a') !== -1 && n === 12){ + n = 0; + } + } + } + + tp_inst.control.value(tp_inst, tp_inst[f+'_slider'], litem, n); + + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + }).css({ + cursor: 'pointer', + width: (100 / gridSize[litem]) + '%', + textAlign: 'center', + overflow: 'hidden' + }); + } // end if grid > 0 + } // end for loop + + // Add timezone options + this.timezone_select = $tp.find('.ui_tpicker_timezone').append('').find("select"); + $.fn.append.apply(this.timezone_select, + $.map(o.timezoneList, function(val, idx) { + return $("