Przeglądaj źródła

xero sync with ui flash completed

master
patrick 6 lat temu
rodzic
commit
c612dae333
11 zmienionych plików z 1040 dodań i 97 usunięć
  1. +18
    -0
      NdisPrice.php
  2. +27
    -2
      TimeSheet.php
  3. +81
    -7
      css/xeroc.css
  4. +55
    -0
      html/bts_client_invoice_template.html
  5. +0
    -0
      html/bts_csv_template.html
  6. +72
    -64
      html/bts_staff_hours_template.html
  7. +4
    -0
      html/timesheet.html
  8. BIN
      img/arrow.gif
  9. +240
    -0
      js/scrollintoview.js
  10. +244
    -3
      js/xeroc.js
  11. +299
    -21
      ts.php

+ 18
- 0
NdisPrice.php Wyświetl plik

@@ -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 "";
}
}

+ 27
- 2
TimeSheet.php Wyświetl plik

@@ -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);
}

+ 81
- 7
css/xeroc.css Wyświetl plik

@@ -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 */
}
}

#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;
}



+ 55
- 0
html/bts_client_invoice_template.html Wyświetl plik

@@ -0,0 +1,55 @@
<tr id="invoice_{{client_login}}" class="invoice_detail_row">
<td class='sync_detail client_invoice' data-client-id='{{client_login}}'>
<table id="p{{client_login}" data-client-login="{{client_login}}" class="clientinvoice">
<thead>
<tr>
<th class="client_invoice"><i class="vc_tta-icon fa fa-minus-square"></i> {{client_name}}</th>
<th class="client_invoice">Service</th>
<th class="client_invoice">Start</th>
<th class="client_invoice">Finish</th>
<th class="client_invoice">Hours</th>
<th class="client_invoice">Unit Price</th>
<th class="client_invoice">Staff</th>
<th class="client_invoice">Price</th>
<th class="client_invoice">Invoice Number</th>
</tr>
</thead>
<tfoot>
<tr>
<td >
</td>
<td colspan="7"></td>
<td class='invoice_button' rowspan="rowspan">
<div class="wpb_wrapper" {{#nojob}}style="visibility:hidden;" {{/nojob}}>
<div class="vc_btn3-container vc_btn3-right invoice_button">
<a data-client-login="{{client_login}}"
class="invoice_button vc_general vc_btn3 vc_btn3-size-md vc_btn3-shape-rounded vc_btn3-style-modern disable_vc_btn3-block vc_btn3-icon-left vc_btn3-color-success"
href="" title="" target="_blank"><i
class="vc_btn3-icon fa fa-refresh"></i> Create Invoice {{client_name}}
</a>
</div>
</div>
</td>
</tr>
</tfoot>
<tbody class="invoiceitems">
{{#jobs}}
<tr>
<td></td>
<td>{{{tos}}}</td>
<td>{{start}}</td>
<td>{{finish}}</td>
<td>{{hours}}</td>
<td>{{unitprice}}</td>
<td>{{staff_name}}</td>
<td>{{price}}</td>
<td class='invoice_nubmer'>
<img class="waiting_invoice_number" src="http://acaresydney.com.au/wp-content/plugins/ts/img/arrow.gif">
<img class="waiting_invoice_number" src="http://acaresydney.com.au/wp-content/plugins/ts/img/xero.png">
</td>
</tr>
{{/jobs}}
</tbody>
</table>
</td>
</tr>

+ 0
- 0
html/bts_csv_template.html Wyświetl plik


+ 72
- 64
html/bts_staff_hours_template.html Wyświetl plik

@@ -1,68 +1,76 @@
{{#lines}}
<tr>
<td>{{staff_name}}</td>
<td>{{rate_name}}</td>
<td class=sync_detail>
<table class='hours'>
<thead id="p{{staff_id}}">
<th>{{days_1}}</th>
<th>{{days_2}}</th>
<th>{{days_3}}</th>
<th>{{days_4}}</th>
<th>{{days_5}}</th>
<th>{{days_6}}</th>
<th>{{days_7}}</th>
<th>{{days_8}}</th>
<th>{{days_9}}</th>
<th>{{days_10}}</th>
<th>{{days_11}}</th>
<th>{{days_12}}</th>
<th>{{days_13}}</th>
<th>{{days_14}}</th>
</thead>
<tbody>
<tr>
<td class='local week1color' id='a_0'>{{local_1}}</td>
<td class='local week1color' id='a_1'>{{local_2}}</td>
<td class='local week1color' id='a_2'>{{local_3}}</td>
<td class='local week1color' id='a_3'>{{local_4}}</td>
<td class='local week1color' id='a_4'>{{local_5}}</td>
<td class='local week1color' id='a_5'>{{local_6}}</td>
<td class='local week1color' id='a_6'>{{local_7}}</td>
<td class='local week2color' id='a_7'>{{local_8}}</td>
<td class='local week2color' id='a_8'>{{local_9}}</td>
<td class='local week2color' id='a_9'>{{local_10}}</td>
<td class='local week2color' id='a_10'>{{local_11}}</td>
<td class='local week2color' id='a_11'>{{local_12}}</td>
<td class='local week2color' id='a_12'>{{local_13}}</td>
<td class='local week2color' id='a_13'>{{local_14}}</td>
</tr>
<tr>
<td colspan="14">
<div style="text-align: center">To Xero
<span class="ticon ticon-arrow-down"></span>
</div>
</td>
</tr>
<tr>
<td class='xero week1color' id='b_0'>{{xero_1}}</td>
<td class='xero week1color' id='b_1'>{{xero_2}}</td>
<td class='xero week1color' id='b_2'>{{xero_3}}</td>
<td class='xero week1color' id='b_3'>{{xero_4}}</td>
<td class='xero week1color' id='b_4'>{{xero_5}}</td>
<td class='xero week1color' id='b_5'>{{xero_6}}</td>
<td class='xero week1color' id='b_6'>{{xero_7}}</td>
<td class='xero week2color' id='b_7'>{{xero_8}}</td>
<td class='xero week2color' id='b_8'>{{xero_9}}</td>
<td class='xero week2color' id='b_9'>{{xero_10}}</td>
<td class='xero week2color' id='b_10'>{{xero_11}}</td>
<td class='xero week2color' id='b_11'>{{xero_12}}</td>
<td class='xero week2color' id='b_12'>{{xero_13}}</td>
<td class='xero week2color' id='b_13'>{{xero_14}}</td>
</tr>
</tbody>
</table>
<tr class="{{Xero_Status}}">
<td rowspan='{{rowspan}}'><strong>{{staff_name}}</strong> <br>Total: {{local_total}} hours</td>
<td>Earnings</td>
<td class=sync_detail> <table class='hours'>
<thead id="p{{staff_id}}">
<th>{{days_1}}</th>
<th>{{days_2}}</th>
<th>{{days_3}}</th>
<th>{{days_4}}</th>
<th>{{days_5}}</th>
<th>{{days_6}}</th>
<th>{{days_7}}</th>
<th>{{days_8}}</th>
<th>{{days_9}}</th>
<th>{{days_10}}</th>
<th>{{days_11}}</th>
<th>{{days_12}}</th>
<th>{{days_13}}</th>
<th>{{days_14}}</th>
</thead>
</table>
</td>
<td style="vertical-align:bottom;">{{Xero_Status}}</td>
<td rowspan='{{rowspan}}' style="vertical-align:bottom;">{{remote_total}} hours<br><strong>{{Xero_Status}}</strong></td>
</tr>
{{#ratetype}}
<tr>
<td>{{rate_name}}</td>
<td class=sync_detail>
<table class='hours'>
<tbody>
<tr>
<td class='local week1color' id='a_0'>{{local_1}}</td>
<td class='local week1color' id='a_1'>{{local_2}}</td>
<td class='local week1color' id='a_2'>{{local_3}}</td>
<td class='local week1color' id='a_3'>{{local_4}}</td>
<td class='local week1color' id='a_4'>{{local_5}}</td>
<td class='local week1color' id='a_5'>{{local_6}}</td>
<td class='local week1color' id='a_6'>{{local_7}}</td>
<td class='local week2color' id='a_7'>{{local_8}}</td>
<td class='local week2color' id='a_8'>{{local_9}}</td>
<td class='local week2color' id='a_9'>{{local_10}}</td>
<td class='local week2color' id='a_10'>{{local_11}}</td>
<td class='local week2color' id='a_11'>{{local_12}}</td>
<td class='local week2color' id='a_12'>{{local_13}}</td>
<td class='local week2color' id='a_13'>{{local_14}}</td>
</tr>
<tr>
<td colspan="14">
<div style="text-align: center">To Xero
<span class="ticon ticon-arrow-down"></span>
</div>
</td>
</tr>
<tr>
<td class='xero week1color {{xero_1_mismatch}}' id='b_0'>{{xero_1}}</td>
<td class='xero week1color {{xero_2_mismatch}}' id='b_1'>{{xero_2}}</td>
<td class='xero week1color {{xero_3_mismatch}}' id='b_2'>{{xero_3}}</td>
<td class='xero week1color {{xero_4_mismatch}}' id='b_3'>{{xero_4}}</td>
<td class='xero week1color {{xero_5_mismatch}}' id='b_4'>{{xero_5}}</td>
<td class='xero week1color {{xero_6_mismatch}}' id='b_5'>{{xero_6}}</td>
<td class='xero week1color {{xero_7_mismatch}}' id='b_6'>{{xero_7}}</td>
<td class='xero week2color {{xero_8_mismatch}}' id='b_7'>{{xero_8}}</td>
<td class='xero week2color {{xero_9_mismatch}}' id='b_8'>{{xero_9}}</td>
<td class='xero week2color {{xero_10_mismatch}}' id='b_9'>{{xero_10}}</td>
<td class='xero week2color {{xero_11_mismatch}}' id='b_10'>{{xero_11}}</td>
<td class='xero week2color {{xero_12_mismatch}}' id='b_11'>{{xero_12}}</td>
<td class='xero week2color {{xero_13_mismatch}}' id='b_12'>{{xero_13}}</td>
<td class='xero week2color {{xero_14_mismatch}}' id='b_13'>{{xero_14}}</td>
</tr>
</tbody>
</table>
</td>
</tr>
{{/ratetype}}
{{/lines}}

+ 4
- 0
html/timesheet.html Wyświetl plik

@@ -169,15 +169,19 @@
<img src="#">
</div>
<div class='statusbar titlebar_gradient'>
<a id='xeroc' href="/xeroc/" target="_blank">
<div class='xero'>
<i class="ticon ticon-times"></i>
</div>
</a>
<div class='wifi'>
<i class="ticon ticon-times"></i>
</div>
<a id='ndiscsv' href="/ndiscsv/" target="_blank">
<div class='csv'>
<i class="ticon ticon-times"></i>
</div>
</a>
<div class='wages'>
$
<div name='number'>

BIN
img/arrow.gif Wyświetl plik

Before After
Width: 600  |  Height: 600  |  Size: 47KB

+ 240
- 0
js/scrollintoview.js Wyświetl plik

@@ -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) {
/// <summary>Scrolls the first element in the set into view by scrolling its closest scrollable parent.</summary>
/// <param name="options" type="Object">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)
/// </param>
/// <return type="jQuery">Returns the same jQuery set that this function was run on.</return>

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);
};
});
});

+ 244
- 3
js/xeroc.js Wyświetl plik

@@ -25,9 +25,15 @@
$( ".boundary_datepicker" ).datepicker("option", "dateFormat", "yy-mm-dd");
}
function loading()
{
return "<tr class='loading' ><td colspan=4 class='loading'><img src='"+bts().load_job_img +"'><br><h1>Sync to Xero</h1></td></tr>";
}
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);
})(jQuery);

+ 299
- 21
ts.php Wyświetl plik

@@ -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 = <<<ZOT
<tr id="nameonly_%s" data-client-id='%s' class="invoice_nameonly_row">
<td class="client_nameonly" data-client-id='%s'>
<i class="vc_tta-icon fa fa-plus-square"></i> %s
</td>
</tr>
<tr id="dummyui_%s" data-client-id='%s' class="invoice_nameonly_row_dummy">
<td class="client_nameonly" data-client-id='%s'>
<i class="vc_tta-icon fa fa-plus-square"></i> %s is loading .... please wait...
</td>
</tr>
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("<a href='https://go.xero.com/AccountsReceivable/Edit.aspx?InvoiceID=%s' target='_blank'>%s</a>",
$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");

Ładowanie…
Anuluj
Zapisz