diff --git a/NdisPrice.php b/NdisPrice.php index e49d01d..4082646 100644 --- a/NdisPrice.php +++ b/NdisPrice.php @@ -61,4 +61,22 @@ class NdisPrice{ } return ""; } + + public function get_tos_price($ndis_code) + { + foreach ($this->tos as $r){ + if ($ndis_code == $r->code) + return (float) $r->price; + } + return 0; + } + + public function get_tos_unit($ndis_code) + { + foreach ($this->tos as $r){ + if ($ndis_code == $r->code) + return $r->unit; + } + return ""; + } } \ No newline at end of file diff --git a/TimeSheet.php b/TimeSheet.php index df5d37f..c956f2b 100644 --- a/TimeSheet.php +++ b/TimeSheet.php @@ -28,6 +28,10 @@ class TimeSheet{ // )); } + public function refresh_remote(){ + $this->get_remote_timesheets(); + } + private function get_remote_timesheets() { $this->remote_timesheets = $this->xero->load('PayrollAU\\Timesheet') @@ -80,6 +84,10 @@ class TimeSheet{ { $to_save=[]; foreach ( $this->local_timesheets as $t){ + $buddy = $this->get_buddy_timesheet_by_ts($t); + if ($buddy != NULL && $buddy->getStatus() != "DRAFT"){ + continue;//we encountered approved timesheet; + } $t->setDirty('EmployeeID'); $t->setDirty('StartDate'); $t->setDirty('EndDate'); @@ -107,6 +115,12 @@ class TimeSheet{ ->setEndDate(new \DateTime($this->end_date)) ->setStatus("DRAFT"); + foreach($ts->getTimesheetLines() as $line){ + $eid = $line->getEarningsRateID(); + $zeroline= $this->create_empty_timesheet_lines($eid); + $empty->addTimesheetLine($zeroline); + } + if ( $ts->getStatus() == "DRAFT" ){//good, we can save it; $to_save[] = $empty;//add it to save }else{ @@ -121,6 +135,15 @@ class TimeSheet{ $this->xero->saveAll($to_save, false); } + private function create_empty_timesheet_lines($EarningsRateID) + { + $line = new \XeroPHP\Models\PayrollAU\Timesheet\TimesheetLine($xero); + $line->setEarningsRateID($EarningsRateID); + for ($i=0; $i<14; $i++) + $line->addNumberOfUnit(0); + return $line; + } + private function get_timesheet_id_by_employee_id($id) { foreach ($this->remote_timesheets as $ts) @@ -138,9 +161,10 @@ class TimeSheet{ return $this->warning_timesheets; } - private function approve_all(){ + public function approve_all(){ $to_save=[]; - foreach ( $this->local_timesheets as $t){ + foreach ( $this->remote_timesheets as $t){ + if($t->getStatus() == 'DRAFT'){ $t->setDirty('EmployeeID'); $t->setDirty('StartDate'); $t->setDirty('EndDate'); @@ -150,6 +174,7 @@ class TimeSheet{ $t->setDirty('TimesheetID'); $t->setStatus('APPROVED'); $to_save[]=$t; + } } $this->xero->saveAll($to_save, false); } diff --git a/css/xeroc.css b/css/xeroc.css index fc1b851..d5cb828 100644 --- a/css/xeroc.css +++ b/css/xeroc.css @@ -1,17 +1,32 @@ @CHARSET "UTF-8"; +.blink_me { + animation: blinker 0.3s linear infinite; +} + +@keyframes blinker { + 50% { + opacity: 0; + } +} + .container{ width:100%; } -#cstart, #cfinish, #paydate{ - color: black; - font-weight:900; +#cstart, #cfinish, #paydate, +#invoice_start, #invoice_finish{ width:100%; - background-color: #EDEDED; border:none; + text-align: center; + color: green; + font-weight: 900; + background: #f0fbff; + font-size: 1.5em; } + + .hidden{ display:none; } @@ -20,6 +35,12 @@ td.sync_detail{ padding:0px; } +table.staffhours, +table.clientinvoice{ + background-color:white; +} + +div.invoice_button, table.hours{ margin-bottom:0px; } @@ -83,7 +104,7 @@ table.blueTable tfoot .links a{ } -.week1color { +tr.DRAFT .week1color { color: black; /* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#ebe9f9+0,d8d0ef+50,cec7ec+51,c1bfea+100;Purple+3D+%231 */ @@ -95,7 +116,7 @@ table.blueTable tfoot .links a{ } -.week2color { +tr.DRAFT .week2color { color: white; /* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#627d4d+0,1f3b08+100;Olive+3D */ @@ -104,4 +125,57 @@ table.blueTable tfoot .links a{ background: -webkit-linear-gradient(top, #627d4d 0%,#1f3b08 100%); /* Chrome10-25,Safari5.1-6 */ background: linear-gradient(to bottom, #627d4d 0%,#1f3b08 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#627d4d', endColorstr='#1f3b08',GradientType=0 ); /* IE6-9 */ -} \ No newline at end of file +} + +#staff tr.loading, +#staff td.loading{ + text-align:center; + height:500px; +} + +td.loading img{ + margin-top:200px; +} + +table.hours th, +table.hours td{ + text-align: center; + width:7.14% ; +} + +.mismatch { + background-color:yellow; + color:red; + font-size:1.3em; + font-weight:900; +} + +td.invoice_nubmer{ + text-align:center; +} +img.waiting_invoice_number{ + height:32px; + display:none; +} + +.invoice_nameonly_row{ + cursor:pointer; +} +.invoice_nameonly_row:hover{ + background-color:yellow; +} + +.invoice_detail_row th{ + cursor:pointer; +} +.invoice_detail_row th:hover{ + color:green; +} +.invoice_nameonly_row_dummy{ + color:white; + background-color: black; + display:none; + animation: blinker 0.3s linear infinite; +} + + diff --git a/html/bts_client_invoice_template.html b/html/bts_client_invoice_template.html new file mode 100644 index 0000000..96ffde3 --- /dev/null +++ b/html/bts_client_invoice_template.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + {{#jobs}} + + + + + + + + + + + + {{/jobs}} + +
{{client_name}}ServiceStartFinishHoursUnit PriceStaffPriceInvoice Number
+ + +
{{{tos}}}{{start}}{{finish}}{{hours}}{{unitprice}}{{staff_name}}{{price}} + + +
+ + \ No newline at end of file diff --git a/html/bts_csv_template.html b/html/bts_csv_template.html new file mode 100644 index 0000000..e69de29 diff --git a/html/bts_staff_hours_template.html b/html/bts_staff_hours_template.html index 46a8f67..f23b011 100644 --- a/html/bts_staff_hours_template.html +++ b/html/bts_staff_hours_template.html @@ -1,68 +1,76 @@ {{#lines}} - - {{staff_name}} - {{rate_name}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{days_1}}{{days_2}}{{days_3}}{{days_4}}{{days_5}}{{days_6}}{{days_7}}{{days_8}}{{days_9}}{{days_10}}{{days_11}}{{days_12}}{{days_13}}{{days_14}}
{{local_1}}{{local_2}}{{local_3}}{{local_4}}{{local_5}}{{local_6}}{{local_7}}{{local_8}}{{local_9}}{{local_10}}{{local_11}}{{local_12}}{{local_13}}{{local_14}}
-
To Xero - -
-
{{xero_1}}{{xero_2}}{{xero_3}}{{xero_4}}{{xero_5}}{{xero_6}}{{xero_7}}{{xero_8}}{{xero_9}}{{xero_10}}{{xero_11}}{{xero_12}}{{xero_13}}{{xero_14}}
+ + {{staff_name}}
Total: {{local_total}} hours + Earnings + + + + + + + + + + + + + + + + + +
{{days_1}}{{days_2}}{{days_3}}{{days_4}}{{days_5}}{{days_6}}{{days_7}}{{days_8}}{{days_9}}{{days_10}}{{days_11}}{{days_12}}{{days_13}}{{days_14}}
- {{Xero_Status}} + {{remote_total}} hours
{{Xero_Status}} + {{#ratetype}} + + {{rate_name}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{local_1}}{{local_2}}{{local_3}}{{local_4}}{{local_5}}{{local_6}}{{local_7}}{{local_8}}{{local_9}}{{local_10}}{{local_11}}{{local_12}}{{local_13}}{{local_14}}
+
To Xero + +
+
{{xero_1}}{{xero_2}}{{xero_3}}{{xero_4}}{{xero_5}}{{xero_6}}{{xero_7}}{{xero_8}}{{xero_9}}{{xero_10}}{{xero_11}}{{xero_12}}{{xero_13}}{{xero_14}}
+ + + {{/ratetype}} {{/lines}} \ No newline at end of file diff --git a/html/timesheet.html b/html/timesheet.html index 3ef2f4e..a1dd7d7 100644 --- a/html/timesheet.html +++ b/html/timesheet.html @@ -169,15 +169,19 @@
+
+
+
+
$
diff --git a/img/arrow.gif b/img/arrow.gif new file mode 100644 index 0000000..0ca4745 Binary files /dev/null and b/img/arrow.gif differ diff --git a/js/scrollintoview.js b/js/scrollintoview.js new file mode 100644 index 0000000..0edd85c --- /dev/null +++ b/js/scrollintoview.js @@ -0,0 +1,240 @@ +/*! + * jQuery scrollintoview() plugin and :scrollable selector filter + * + * Version 1.9.4 (06 April 2016) + * Requires jQuery 1.4 or newer + * + * Copyright (c) 2011 Robert Koritnik + * Licensed under the terms of the MIT license + * http://www.opensource.org/licenses/mit-license.php + */ + +!function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof exports === 'object') { + factory(require('jquery')); + } else { + factory(root.jQuery); + } +} +(this, function($) { + var converter = { + vertical: { x: false, y: true }, + horizontal: { x: true, y: false }, + both: { x: true, y: true }, + x: { x: true, y: false }, + y: { x: false, y: true } + }; + + var settings = { + duration: "fast", + direction: "both", + viewPadding: 0 + }; + + var rootrx = /^(?:html)$/i; + + // gets border dimensions + var borders = function(domElement, styles) { + styles = styles || (document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(domElement, null) : domElement.currentStyle); + var px = document.defaultView && document.defaultView.getComputedStyle ? true : false; + var b = { + top: (parseFloat(px ? styles.borderTopWidth : $.css(domElement, "borderTopWidth")) || 0), + left: (parseFloat(px ? styles.borderLeftWidth : $.css(domElement, "borderLeftWidth")) || 0), + bottom: (parseFloat(px ? styles.borderBottomWidth : $.css(domElement, "borderBottomWidth")) || 0), + right: (parseFloat(px ? styles.borderRightWidth : $.css(domElement, "borderRightWidth")) || 0) + }; + return { + top: b.top, + left: b.left, + bottom: b.bottom, + right: b.right, + vertical: b.top + b.bottom, + horizontal: b.left + b.right + }; + }; + + var dimensions = function($element) { + var elem = $element[0], + isRoot = rootrx.test(elem.nodeName), + $elem = isRoot ? $(window) : $element; + return { + border: isRoot ? { top: 0, left: 0, bottom: 0, right: 0 } : borders(elem), + scroll: { + top: $elem.scrollTop(), + left: $elem.scrollLeft(), + maxtop: elem.scrollHeight - elem.clientHeight, + maxleft: elem.scrollWidth - elem.clientWidth + }, + scrollbar: isRoot + ? { right: 0, bottom: 0 } + : { + right: $elem.innerWidth() - elem.clientWidth, + bottom: $elem.innerHeight() - elem.clientHeight + }, + rect: isRoot ? { top: 0, left: 0, bottom: elem.clientHeight, right: elem.clientWidth } : elem.getBoundingClientRect() + }; + }; + + $.fn.extend({ + scrollintoview: function(options) { + /// Scrolls the first element in the set into view by scrolling its closest scrollable parent. + /// Additional options that can configure scrolling: + /// duration (default: "fast") - jQuery animation speed (can be a duration string or number of milliseconds) + /// direction (default: "both") - select possible scrollings ("vertical" or "y", "horizontal" or "x", "both") + /// complete (default: none) - a function to call when scrolling completes (called in context of the DOM element being scrolled) + /// + /// Returns the same jQuery set that this function was run on. + + options = $.extend({}, settings, options); + options.direction = converter[typeof (options.direction) === "string" && options.direction.toLowerCase()] || converter.both; + + if (typeof options.viewPadding == "number") { + options.viewPadding = { x: options.viewPadding, y: options.viewPadding }; + } else if (typeof options.viewPadding == "object") { + if (options.viewPadding.x == undefined) { + options.viewPadding.x = 0; + } + if (options.viewPadding.y == undefined) { + options.viewPadding.y = 0; + } + } + + var dirStr = ""; + if (options.direction.x === true) dirStr = "horizontal"; + if (options.direction.y === true) dirStr = dirStr ? "both" : "vertical"; + + var el = this.eq(0); + var scroller = el.parent().closest(":scrollable(" + dirStr + ")"); + + // check if there's anything to scroll in the first place + if (scroller.length > 0) { + scroller = scroller.eq(0); + + var dim = { + e: dimensions(el), + s: dimensions(scroller) + }; + + var rel = { + top: dim.e.rect.top - (dim.s.rect.top + dim.s.border.top), + bottom: dim.s.rect.bottom - dim.s.border.bottom - dim.s.scrollbar.bottom - dim.e.rect.bottom, + left: dim.e.rect.left - (dim.s.rect.left + dim.s.border.left), + right: dim.s.rect.right - dim.s.border.right - dim.s.scrollbar.right - dim.e.rect.right + }; + + var animProperties = {}; + + // vertical scroll + if (options.direction.y === true) { + if (rel.top < 0) { + animProperties.scrollTop = Math.max(0, dim.s.scroll.top + rel.top - options.viewPadding.y); + } else if (rel.top > 0 && rel.bottom < 0) { + animProperties.scrollTop = Math.min(dim.s.scroll.top + Math.min(rel.top, -rel.bottom) + options.viewPadding.y, dim.s.scroll.maxtop); + } + } + + // horizontal scroll + if (options.direction.x === true) { + if (rel.left < 0) { + animProperties.scrollLeft = Math.max(0, dim.s.scroll.left + rel.left - options.viewPadding.x); + } else if (rel.left > 0 && rel.right < 0) { + animProperties.scrollLeft = Math.min(dim.s.scroll.left + Math.min(rel.left, -rel.right) + options.viewPadding.x, dim.s.scroll.maxleft); + } + } + + // scroll if needed + if (!$.isEmptyObject(animProperties)) { + var scrollExpect = {}, + scrollListener = scroller; + + if (rootrx.test(scroller[0].nodeName)) { + scroller = $("html,body"); + scrollListener = $(window); + } + + function animateStep(now, tween) { + scrollExpect[tween.prop] = Math.floor(now); + }; + + function onscroll(event) { + $.each(scrollExpect, function(key, value) { + if (Math.floor(scrollListener[key]()) != Math.floor(value)) { + options.complete = null; // don't run complete function if the scrolling was interrupted + scroller.stop('scrollintoview'); + } + }); + } + + scrollListener.on('scroll', onscroll); + + scroller + .stop('scrollintoview') + .animate(animProperties, { duration: options.duration, step: animateStep, queue: 'scrollintoview' }) + .eq(0) // we want function to be called just once (ref. "html,body") + .queue('scrollintoview', function(next) { + scrollListener.off('scroll', onscroll); + $.isFunction(options.complete) && options.complete.call(scroller[0]); + next(); + }) + + scroller.dequeue('scrollintoview'); + } else { + // when there's nothing to scroll, just call the "complete" function + $.isFunction(options.complete) && options.complete.call(scroller[0]); + } + } + + // return set back + return this; + } + }); + + var scrollValue = { + auto: true, + scroll: true, + visible: false, + hidden: false + }; + + var scroll = function(element, direction) { + direction = converter[typeof (direction) === "string" && direction.toLowerCase()] || converter.both; + var styles = (document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(element, null) : element.currentStyle); + var overflow = { + x: scrollValue[styles.overflowX.toLowerCase()] || false, + y: scrollValue[styles.overflowY.toLowerCase()] || false, + isRoot: rootrx.test(element.nodeName) + }; + + // check if completely unscrollable (exclude HTML element because it's special) + if (!overflow.x && !overflow.y && !overflow.isRoot) { + return false; + } + + var size = { + height: { + scroll: element.scrollHeight, + client: element.clientHeight + }, + width: { + scroll: element.scrollWidth, + client: element.clientWidth + }, + // check overflow.x/y because iPad (and possibly other tablets) don't dislay scrollbars + scrollableX: function() { + return (overflow.x || overflow.isRoot) && this.width.scroll > this.width.client; + }, + scrollableY: function() { + return (overflow.y || overflow.isRoot) && this.height.scroll > this.height.client; + } + }; + return direction.y && size.scrollableY() || direction.x && size.scrollableX(); + }; + + $.expr[":"].scrollable = $.expr.createPseudo(function(direction) { + return function(element) { + return scroll(element, direction); + }; + }); +}); diff --git a/js/xeroc.js b/js/xeroc.js index 023127b..7e01fd8 100644 --- a/js/xeroc.js +++ b/js/xeroc.js @@ -25,9 +25,15 @@ $( ".boundary_datepicker" ).datepicker("option", "dateFormat", "yy-mm-dd"); } + function loading() + { + return "

Sync to Xero

"; + } + + function display_hour_lines(response) { - $('#staff').html(); + $('#staff').html(''); var temp = $('#bts_staff_hours_template').html(); var html = Mustache.render(temp, response); $('#staff').append(html); @@ -41,6 +47,7 @@ } function get_timesheet_from_xero(){ + $('#staff').html(loading()); $.post(bts().ajax_url, { // POST request _ajax_nonce: bts().nonce, // nonce action: "get_timesheet_from_xero", // action @@ -57,13 +64,13 @@ } function sync_timesheet_from_xero(){ + $('#staff').html(loading()); $.post(bts().ajax_url, { // POST request _ajax_nonce: bts().nonce, // nonce action: "get_timesheet_from_xero", // action sync: true, }).done(function(response){ set_payroll_calendar(response.payroll_calendar); - console.log("%o", response); display_hour_lines(response); }).fail(function(){ console.warn('failed'); @@ -71,9 +78,243 @@ console.log('completed'); }); } + + function approve_all_timesheet() + { + $.post(bts().ajax_url, { // POST request + _ajax_nonce: bts().nonce, // nonce + action: "approve_all_timesheet", // action + }).done(function(response){ + if (response.status == 'success') + alert("approve all succeed"); + else + alert(response.err); + }).fail(function(){ + alert("Network Error, cannot approve all"); + }).always(function(){ + console.log('completed'); + }); + + } $('#sync_timesheet').click(function(){ sync_timesheet_from_xero(); }); + $('#approve_all').click(function(){ + approve_all_timesheet(); + }); + + function display_invoice_items_test(response) + { + var template = $('#bts_client_invoice_template').html(); + for (var i=1; i<10; i++){ + data = { + client_name: "Martin", + jobs:[ + { + tos: "service a " + i, + staff_name: "joe", + start: "2019-07-01", + finish: "2019-07-14", + hours: i, + price: 336, + }, + { + tos: "service b " + i, + staff_name: "joe dne", + start: "2019-07-01", + finish: "2019-07-14", + hours: i, + price: 16, + } + ] + } + html = Mustache.render(template, data); + $('#clientinvoice').append(html); + } + } + + function display_invoice_items(selector, response) + { + var template = $('#bts_client_invoice_template').html(); + html = Mustache.render(template, response); + el = $(html); + $(selector).after(el); + //el.SlideDown(); + el.show(); + } + + function get_invoice_item(selector, client_id) + { + $.post(bts().ajax_url, { // POST request + _ajax_nonce: bts().nonce, // nonce + action: "get_invoice_item", // action + client: client_id, + start: get_invoice_start(), + finish: get_invoice_finish(), + }).done(function(response){ + if (response.status == 'success'){ + display_invoice_items(selector, response); + $(selector).hide(); + }else{ + alert(response.err); + } + }).fail(function(){ + alert("Network Error, cannot approve all"); + }).always(function(){ + console.log('completed'); + }); + } + + function create_invoice_number(client_id) + { + start_showing_invoice_request(client_id); + $.post(bts().ajax_url, { // POST request + _ajax_nonce: bts().nonce, // nonce + action: "create_invoice_in_xero", // action + client: client_id, + start: get_invoice_start(), + finish: get_invoice_finish(), + }).done(function(response){ + if (response.status == 'success'){ + show_invoice_number(response); + }else{ + alert(response.err); + } + }).fail(function(){ + alert("Network Error, cannot approve all"); + }).always(function(){ + console.log('completed'); + }); + } + + function start_showing_invoice_request(client_id) + { + $('td.invoice_nubmer img').show(); + animate_into_top('#invoice_' + client_id); + return; + $('#invoice_' + client_id).scrollintoview({ + duration: 2500, + direction: "vertical", + viewPadding: { y: 10 }, + complete: function() { + // highlight the element so user's focus gets where it needs to be + } + }); + } + + function animate_into_top(el) + { + var offset = $(el).offset(); // Contains .top and .left + offset.left -= 20; + offset.top -= 20; + $('html, body').animate({ + scrollTop: offset.top, + scrollLeft: offset.left + },1000); + } + + function show_invoice_number(response) + { + $('td.invoice_nubmer').html(response.invoice_number); + $('td.invoice_button div').hide(); + } + + function get_invoice_start() + { + return $('#invoice_start').attr('value'); + } + function get_invoice_finish() + { + return $('#invoice_finish').attr('value'); + } + + $(document).on('click', 'td.client_nameonly', function(){ + var id = $(this).attr('data-client-id'); + $('#nameonly_' + id).hide(); + $('#dummyui_' + id).show(); + if( $('#invoice_' + id).length == 0 ){ + get_invoice_item('#dummyui_' + id, id); + }else{ + $('#dummyui_' + id).hide(); + $('#invoice_' + id).show(); + } + }); + + $(document).on('click', 'th.client_invoice', function(){ + var id = $(this).closest('td.client_invoice').attr('data-client-id'); + $('#nameonly_' + id).show(); + $('#invoice_' + id).hide(); + }); + + $(document).on('hide','#maintabs',function(){ + alert('abc'); + }); + + $(document).on("afterShow.vc.accordion", function(e, opt) { + console.log("%o, %o", e, opt); + if (e.target.hash =="#1565353205981-c3582e44-83d2"){ + get_timesheet_from_xero(); + } + }) + + function format_date(date){ + var dd = date.getDate(); + var mm = date.getMonth() + 1; //January is 0! + + var yyyy = date.getFullYear(); + if (dd < 10) { + dd = '0' + dd; + } + if (mm < 10) { + mm = '0' + mm; + } + return yyyy + '-' + mm + '-' +dd ; + } + + function daysInMonth (month, year) { + return new Date(year, month, 0).getDate(); + } + + function setup_invoice_start_finish() + { + var date = new Date(); + var firstDay = new Date(date.getFullYear(), + date.getMonth(), 1); + var lastDay = new Date(date.getFullYear(), + date.getMonth(), daysInMonth(date.getMonth()+1, + date.getFullYear())); + $('#invoice_start').attr('value', format_date(firstDay)); + $('#invoice_finish').attr('value', format_date(lastDay)); + } + + $('#invoice_start').change(function(){ + clear_all_invoice(); + }); + $('#invoice_finish').change(function(){ + clear_all_invoice(); + }); + + $(document).on('click', 'a.invoice_button', function(e){ + e.stopPropagation(); + var id = $(this).attr('data-client-login'); + create_invoice_number(id); + return false; + }); + + function clear_all_invoice() + { + if ( $(".invoice_detail_row").length >0 ){ + if (!confirm("Change Date will clear all invoice details")) + return; + } + $(".invoice_detail_row").remove(); + $(".invoice_nameonly_row").show(); + } + + + datebox(); + setup_invoice_start_finish(); + /*_____________________________________________*/ }); -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/ts.php b/ts.php index d19f433..fd7d777 100644 --- a/ts.php +++ b/ts.php @@ -49,7 +49,9 @@ class AcareOffice{ add_shortcode( 'bts_feedback_card', array($this, 'bts_feedback_card')); add_shortcode( 'bb_timesheet_canvas', array($this, 'bb_timesheet_canvas')); add_shortcode( 'bts_staff_hours_template', array($this, 'bts_staff_hours_template')); - + add_shortcode( 'bts_client_invoice_template', array($this, 'bts_client_invoice_template')); + add_shortcode( 'bts_csv_template', array($this, 'bts_csv_template')); + add_shortcode( 'bts_invoiced_client', array($this, 'bts_invoiced_client')); //user profile page add_shortcode( 'bts_user_name', array($this,'bts_user_name')); @@ -78,6 +80,10 @@ class AcareOffice{ add_action('wp_ajax_nopriv_client_ack_job', array($this,'client_ack_job' )); add_action('wp_ajax_get_timesheet_from_xero', array($this,'get_timesheet_from_xero' )); + add_action('wp_ajax_approve_all_timesheet', array($this,'approve_all_timesheet' )); + add_action('wp_ajax_get_invoice_item', array($this,'get_invoice_item' )); + add_action('wp_ajax_create_invoice_in_xero', array($this,'create_invoice_in_xero' )); + // hook add_rewrite_rules function into rewrite_rules_array add_filter('rewrite_rules_array', array($this,'my_add_rewrite_rules')); @@ -112,7 +118,32 @@ class AcareOffice{ $this->xero->init_wp(); //$abc = new AddrMap("01515b52-6936-46b2-a000-9ad4cd7a5b50", "0768db6d-e5f4-4b45-89a2-29f7e8d2953c"); - $abc = new AddrMap("122eb1d0-d8c4-4fc3-8bf8-b7825bee1a01", "0768db6d-e5f4-4b45-89a2-29f7e8d2953c"); + //$abc = new AddrMap("122eb1d0-d8c4-4fc3-8bf8-b7825bee1a01", "0768db6d-e5f4-4b45-89a2-29f7e8d2953c"); + + $this->check_csv_download(); + } + + private function check_csv_download() + { + $url = $_SERVER['REQUEST_URI']; + $matches=[]; + preg_match("/\/ndiscsv\/start-([^\/]+)\/finish-([^\/]+)\/?$/", $url, $matches); + if ( $matches !=3 || $_SERVER['REQUEST_URI'] != $matches[0] ) + return; + $start = $matches[1]; + $finish = $matches[2]; + $filename="{$start}___{$finish}.csv"; + + header("Expires: 0"); + header("Cache-Control: no-cache, no-store, must-revalidate"); + header('Cache-Control: pre-check=0, post-check=0, max-age=0', false); + header("Pragma: no-cache"); + header("Content-type: text/csv"); + header("Content-Disposition:attachment; filename=$filename"); + header("Content-Type: application/force-download"); + //readfile(dirname(__FILE__) . "/img/circle.png"); + echo "fuck this file"; + exit(); } @@ -191,6 +222,8 @@ class AcareOffice{ 'feedback_card/([^/]+)/week-([^/]+)/?$' => 'index.php?pagename=feedback_card&bts_user_id=$matches[1]&bts_week_id=$matches[2]', 'feedback_card/([^/]+)/start-([^/]+)/finish-([^/]+)/?$' => 'index.php?pagename=feedback_card&bts_user_id=$matches[1]&bts_job_start=$matches[2]&bts_job_finish=$matches[3]', + 'ndiscsv/start-([^/]+)/finish-([^/]+)/?$' => 'index.php?pagename=ndiscsv&bts_job_start=$matches[1]&bts_job_finish=$matches[2]', + ); $aRules = $aNewRules + $aRules; return $aRules; @@ -460,6 +493,7 @@ class AcareOffice{ wp_enqueue_style( 'bts_xeroc', plugins_url('css/xeroc.css', __FILE__)); wp_enqueue_script( 'bts_xeroc', plugins_url('js/xeroc.js', __FILE__), array( 'jquery' , 'bts' )); wp_enqueue_script('mustache', plugins_url('js/mustache.min.js', __FILE__), array('jquery')); + wp_enqueue_script('scrollintoview', plugins_url('js/scrollintoview.js', __FILE__), array('jquery')); global $wp_scripts; wp_enqueue_script('jquery-ui-datepicker'); @@ -610,6 +644,41 @@ class AcareOffice{ public function bts_staff_hours_template($attr){ return $this->template('bts_staff_hours_template', 'bts_staff_hours_template.html'); } + + public function bts_client_invoice_template($attr){ + return $this->template('bts_client_invoice_template', 'bts_client_invoice_template.html'); + } + + public function bts_csv_template($attr){ + return $this->template('bts_csv_template', 'bts_csv_template.html'); + } + + public function bts_invoiced_client($attr) + { + $result = ""; + $users = $users = get_users(array('role' => 'client')); + $row = << + + %s + + + + + %s is loading .... please wait... + + +ZOT; + foreach ($users as $u) + { + $payment = get_user_meta($u->ID, 'payment', true); + if( $payment != 'invoice' ) + continue; //bypass + $result .= sprintf($row, $u->user_login, $u->user_login, $u->user_login, $u->display_name, + $u->user_login, $u->user_login, $u->user_login, $u->display_name); + } + return $result; + } //generate template based on html file private function template($id, $file) { @@ -1029,6 +1098,7 @@ class AcareOffice{ if ($sync){ $xx->set_local_timesheet($local_ts); $xx->save_to_xero(); + $xx->refresh_remote(); } @@ -1047,33 +1117,52 @@ class AcareOffice{ 'staff_name' => $this->get_user_name_by_login ($staff_login), 'staff_id' => $staff_login, 'Xero_Status' => 'Empty', + 'rowspan' =>1, + 'local_total' =>0, + 'remote_total'=>0, + 'ratetype' => [], ); - //for local + $buddy = $xx->get_buddy_timesheets($staff_login, new \DateTime($start), new \DateTime($finish)); + if ($buddy != NULL){ + $item['Xero_Status'] = $buddy->getStatus(); + }else{ + $item['Xero_Status'] = "Not Exist"; + } + + foreach($details as $rate => $hours){ - $item['rate_name'] = $this->get_rate_name_by_id($rate); + $item['rowspan'] ++; + + $ratetype_item=[]; + $ratetype_item['rate_name'] = $this->get_rate_name_by_id($rate); + //for local for ($i=1; $i<=14; $i++) { - $item["local_$i"] = $hours[$i-1]; + $ratetype_item["local_$i"] = $hours[$i-1]; + $item['local_total'] += $hours[$i-1]; } - } - - //for remote - $buddy = $xx->get_buddy_timesheets($staff_login, new \DateTime($start), new \DateTime($finish)); - if ( $buddy != NULL ) - { - $item['Xero_Status'] = $buddy->getStatus(); - $remote_lines = $buddy->getTimesheetLines(); - foreach($remote_lines as $rl) + + //for remote + if ( $buddy != NULL ) { - if ( $rl->getEarningsRateID() == $rate){ - for ($i=1; $i<=14; $i++) - { - $item["xero_$i"] = $rl->getNumberOfUnits()[$i-1]; - } - break;//we found it - } + $remote_lines = $buddy->getTimesheetLines(); + foreach($remote_lines as $rl) + { + if ( $rl->getEarningsRateID() == $rate){ + for ($i=1; $i<=14; $i++) + { + $ratetype_item["xero_$i"] = $rl->getNumberOfUnits()[$i-1]; + $item['remote_total'] += $rl->getNumberOfUnits()[$i-1]; + if ($ratetype_item["xero_$i"] != $ratetype_item["local_$i"]){ + $ratetype_item["xero_{$i}_mismatch"] = "mismatch"; + } + } + break;//we found it + } + } } + $item['ratetype'][] = $ratetype_item; } $item = array_merge($item, $days); @@ -1086,6 +1175,31 @@ class AcareOffice{ wp_send_json($response); } + public function approve_all_timesheet() + { + check_ajax_referer('acaresydney'); + //set up payroll calendar + $pc = $this->xero->get_payroll_calendar(); + + $start = $pc->getStartDate()->format('Y-m-d'); + $finish = new \DateTime($start); + $finish = $finish->modify("+13 days")->format('Y-m-d'); + $paydate = $pc->getPaymentDate()->format('Y-m-d'); + + //prepare response + $response = array( + 'status' => 'success', + 'payroll_calendar' => array( + 'start' => $start, + 'finish' => $finish, + 'paydate'=> $paydate, + ), + ); + $xx = new \Biukop\TimeSheet($this->xero->get_xero_handle(), $finish); + $xx->approve_all(); + wp_send_json($response); + } + static public function get_user_name_by_login($login) { $user = get_user_by('login', $login); @@ -1095,6 +1209,167 @@ class AcareOffice{ return "Invalid Name"; } + + //ajax + public function get_invoice_item() + { + check_ajax_referer('acaresydney'); + $client = $_POST['client']; + //$client = "8cb3d205-6cdc-4187-ae39-9216923dd86d"; + $start = $_POST['start']; //2019-07-01'; + $finish= $_POST['finish'];//2019-07-31'; + + $sql = "SELECT * from $this->table_name WHERE tos != '00_000_0000_0_0' and start>='$start 00:00:00' and start<='$finish 23:59:59' and client='$client' ORDER BY start"; + $rows = $this->db->get_results($sql); + + $response=[ + 'status' =>'success', + 'client_login' => $client, + 'client_name' => $this->get_user_name_by_login($client), + 'jobs'=>[], + 'err'=>'' + ]; + $now = new \DateTime(); + $price = new NdisPrice($now->format("Y"));//current year; + + $summary=[];// by ndis code + foreach($rows as $r){ + $quantity = $this->get_job_hours($r->start, $r->finish); + $unit = $price->get_tos_unit($r->tos); + if ($unit != "Hour") + { + $quantity = 1; + } + $unitprice = $price->get_tos_price($r->tos); + $description = $this->get_job_description_for_invoice($price, $r); + + $data = [ + 'tos' => $price->get_tos_str($r->tos), + 'start' => $r->start, + 'finish' => $r->finish, + 'hours' => $quantity, + 'unitprice'=> $price->get_tos_price($r->tos), + 'staff_name' => $this->get_user_name_by_login($r->staff), + 'price' => sprintf("%.2f", $quantity * $unitprice), + ]; + $response['jobs'][] = $data; + $summary[$r->tos] += $quantity; + } + + if (count($response['jobs']) ==0) + { + $response['nojob'] = true; + } + + foreach ($summary as $key => $val) + { + $response['summary'][] = array( + 'ndis' => $key, + 'tos' => $price->get_tos_str($key), + 'Hours'=> $val, + ); + } + + + wp_send_json($response); + } + + public function create_invoice_in_xero() + { + check_ajax_referer('acaresydney'); + $client = $_POST['client']; + //$client = "8cb3d205-6cdc-4187-ae39-9216923dd86d"; + $start = $_POST['start']; //2019-07-01'; + $finish= $_POST['finish'];//2019-07-31'; + // + + $response=[ + 'status'=>success, + 'invoice_number' => '', + 'err'=> '', + ]; + + try{ + $invoice = $this->create_invoice_by_client($client, $start, $finish); + $response['invoice_number'] = sprintf("%s", + $invoice->getInvoiceID(), $invoice->getInvoiceNumber()); + }catch(\Exception $e){ + $response['status'] = 'error'; + $response['err'] = "XERO Invoice Error"; + } + + wp_send_json($response); + } + + + public function create_invoice_by_client($client_login, $start, $finish) + { + $user = get_user_by('login', $client_login); + if ( !$this->is_client($user) ) + return NULL; + $payment = get_user_meta($user->ID, "payment", true); + if ($payment != "invoice") + return NULL; + + $sql = "SELECT * from $this->table_name WHERE tos != '00_000_0000_0_0' and start>='$start 00:00:00' and start<='$finish 23:59:59' and client='$client_login' ORDER BY start"; + $rows = $this->db->get_results($sql); + + $xero = $this->xero->get_xero_handle(); + + //crate invoice + $invoice = new \XeroPHP\Models\Accounting\Invoice($xero); + $contact= $xero->loadByGUID('Accounting\\Contact', $client_login); + $now = new \DateTime(); + $due = new \DateTime(); + $due->modify("+14 days"); + $invoice->setType("ACCREC") + ->setStatus("DRAFT") + ->setContact($contact) + ->setDate($now) + ->setDueDate($due); + $to_save=[];//all invoices to save; + $price = new NdisPrice($now->format("Y"));//current year; + + foreach($rows as $r){ + $quantity = $this->get_job_hours($r->start, $r->finish); + $unit = $price->get_tos_unit($r->tos); + if ($unit != "Hour") + { + $quantity = 1; + } + $unitprice = $price->get_tos_price($r->tos); + $description = $this->get_job_description_for_invoice($price, $r); + $lineItem = new \XeroPHP\Models\Accounting\Invoice\LineItem($xero); + $lineItem->setDescription($description) + ->setQuantity($quantity) + ->setUnitAmount($unitprice) + ->setAccountCode(220) + ->setTaxType("EXEMPTOUTPUT"); + $invoice->addLineItem($lineItem); + } + + //prevent zero lineitems + if ( count($invoice->getLineItems()) >0 ) + $invoice->save(); + return $invoice; + } + + public function get_job_description_for_invoice($price, $job) + { + $description = sprintf( +'%s +[NDIS code: %s] +Time: %s - %s +By Carer : %s', + $price->get_tos_str($job->tos), + $job->tos, + $job->start, + $job->finish, + $this->get_user_name_by_login($job->staff) + ); + return $description; + } + public function create_timesheet_from_db($start, $finish){ $results = []; @@ -1177,6 +1452,9 @@ if ( defined( 'WP_CLI' ) && WP_CLI ) { \WP_CLI::add_command( 'feedback_url', array($bb, 'feedback_url')); \WP_CLI::add_command( 'produce_invoice', array($bb, 'produce_invoice')); } + +//$bb->class_loader(); +//$bb->create_invoice_by_client("8cb3d205-6cdc-4187-ae39-9216923dd86d", "2019-07-01", "2019-07-31"); //$idx = $bb->convert_date_to_idx("2019-07-02 14:30:00", "2019-07-01", "2019-07-02"); //wp_send_json($idx); //$bb->create_timesheet_from_db("2019-07-01", "2019-07-14"); \ No newline at end of file