├── .gitignore ├── assets ├── fonts │ ├── raty.eot │ ├── raty.ttf │ ├── raty.woff │ └── raty.svg ├── images │ ├── sort_asc.png │ ├── sort_both.png │ ├── sort_desc.png │ ├── star-half.png │ ├── star-off.png │ ├── star-on.png │ ├── codeable-full.png │ ├── sort_asc_disabled.png │ ├── sort_desc_disabled.png │ ├── icon_datepicker_blue.png │ ├── ca-logo.svg │ └── calendar-add.svg ├── css │ ├── jquery.raty.css │ ├── simplemde.min.css │ └── jquery.dataTables.min.css └── js │ ├── jquery.matchHeight-min.js │ ├── offline-exporting.js │ ├── exporting.js │ ├── wpcable.js │ └── highcharts-3d.js ├── screenshot2-client-data.png ├── screenshot1-money-charts.png ├── screenshot3-client-detail-modal.png ├── functions ├── admin-estimate.php ├── admin-tasks.php ├── admin-settings.php └── helpers.php ├── classes ├── deactivator.php ├── object_cache.php ├── tasks.php ├── api_calls.php ├── clients.php ├── api_data.php └── stats.php ├── CHANGELOG.md ├── uninstall.php ├── README.md ├── templates ├── admin-settings.php ├── admin-task-table.php └── admin-estimate.php └── wp-codeable.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /assets/fonts/raty.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/fonts/raty.eot -------------------------------------------------------------------------------- /assets/fonts/raty.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/fonts/raty.ttf -------------------------------------------------------------------------------- /assets/fonts/raty.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/fonts/raty.woff -------------------------------------------------------------------------------- /assets/images/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/sort_asc.png -------------------------------------------------------------------------------- /assets/images/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/sort_both.png -------------------------------------------------------------------------------- /assets/images/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/sort_desc.png -------------------------------------------------------------------------------- /assets/images/star-half.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/star-half.png -------------------------------------------------------------------------------- /assets/images/star-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/star-off.png -------------------------------------------------------------------------------- /assets/images/star-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/star-on.png -------------------------------------------------------------------------------- /screenshot2-client-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/screenshot2-client-data.png -------------------------------------------------------------------------------- /screenshot1-money-charts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/screenshot1-money-charts.png -------------------------------------------------------------------------------- /assets/images/codeable-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/codeable-full.png -------------------------------------------------------------------------------- /assets/images/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/sort_asc_disabled.png -------------------------------------------------------------------------------- /assets/images/sort_desc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/sort_desc_disabled.png -------------------------------------------------------------------------------- /screenshot3-client-detail-modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/screenshot3-client-detail-modal.png -------------------------------------------------------------------------------- /assets/images/icon_datepicker_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeablehq/expertstatsplugin/HEAD/assets/images/icon_datepicker_blue.png -------------------------------------------------------------------------------- /functions/admin-estimate.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /classes/deactivator.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class WpCable_deactivator { 17 | 18 | /** 19 | * Fired on plugin deactivation. 20 | * 21 | * Removes all plugin tables and wp_option data 22 | * 23 | * @since 0.0.3 24 | */ 25 | public static function deactivate() { 26 | self::remove_cronjobs(); 27 | } 28 | 29 | /** 30 | * Remove all scheduled jobs 31 | * 32 | * @since 0.0.3 33 | */ 34 | private static function remove_cronjobs() { 35 | 36 | // find out when the last event was scheduled 37 | $timestamp = wp_next_scheduled( 'wpcable_cronjob' ); 38 | 39 | // unschedule previous event if any 40 | wp_unschedule_event( $timestamp, 'wpcable_cronjob' ); 41 | 42 | // clear cron upon plugin deactivation 43 | wp_clear_scheduled_hook( 'wpcable_cronjob' ); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /assets/css/jquery.raty.css: -------------------------------------------------------------------------------- 1 | .cancel-on-png, .cancel-off-png, .star-on-png, .star-off-png, .star-half-png { 2 | font-size: 2em; 3 | } 4 | 5 | @font-face { 6 | font-family: "raty"; 7 | font-style: normal; 8 | font-weight: normal; 9 | src: url("../fonts/raty.eot"); 10 | src: url("../fonts/raty.eot?#iefix") format("embedded-opentype"); 11 | src: url("../fonts/raty.svg#raty") format("svg"); 12 | src: url("../fonts/raty.ttf") format("truetype"); 13 | src: url("../fonts/raty.woff") format("woff"); 14 | } 15 | 16 | .cancel-on-png, .cancel-off-png, .star-on-png, .star-off-png, .star-half-png { 17 | -moz-osx-font-smoothing: grayscale; 18 | -webkit-font-smoothing: antialiased; 19 | font-family: "raty"; 20 | font-style: normal; 21 | font-variant: normal; 22 | font-weight: normal; 23 | line-height: 1; 24 | speak: none; 25 | text-transform: none; 26 | } 27 | 28 | .cancel-on-png:before { 29 | content: "\e600"; 30 | } 31 | 32 | .cancel-off-png:before { 33 | content: "\e601"; 34 | } 35 | 36 | .star-on-png:before { 37 | content: "\f005"; 38 | } 39 | 40 | .star-off-png:before { 41 | content: "\f006"; 42 | } 43 | 44 | .star-half-png:before { 45 | content: "\f123"; 46 | } 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | *0.0.1* 4 | * Initial build 5 | 6 | *0.0.2* 7 | * Fix database errors when importing due to empty rows. Temporary fix until loop logic can be improved 8 | * Formatted all files for WordPress's style guide 9 | * Spelling fixes 10 | 11 | *0.0.3* 12 | * Added deactivator class for future deactivation logic 13 | * Added uninstall.php to completely remove all data on deletion 14 | * Spelling fixes 15 | 16 | *0.0.4* 17 | * Testing GIT update 18 | 19 | *0.0.5 (2017-03-01)* 20 | * Text changes on charts [#28](https://github.com/codeablehq/expertstatsplugin/issues/28) @defunctl 21 | 22 | *0.0.6 (2017-03-01)* 23 | * Added invalid credentials logic and admin notice [#29](https://github.com/codeablehq/expertstatsplugin/issues/29) @defunctl 24 | * Added WordPress Object Caching to stats and client DB calls [#19](https://github.com/codeablehq/expertstatsplugin/issues/19) @defunctl 25 | 26 | *0.0.7 (2017-03-15)* 27 | * PERT Calculator - @jin0x , @jonathanbossenger 28 | * New charts - @spyrosvl 29 | * Added offline export support via Highcharts JS @spyrosvl 30 | 31 | *0.0.9 (2017-03-23)* 32 | * Average in charts - @spyrosvl 33 | * Condition to change text between best month/day - @spyrosvl 34 | 35 | *0.0.10 (2017-05-18)* 36 | * Fixes for missing user data, thanks @ahanzek 37 | 38 | *0.0.20 (2019-07-07)* 39 | * Update UI layout to match current WordPress standards 40 | * Apply WordPress coding standards to code 41 | -------------------------------------------------------------------------------- /assets/images/calendar-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 | 46 | */ 47 | class wpcable_uninstall { 48 | 49 | /** 50 | * Fired on plugin deactivation. 51 | * 52 | * Removes all plugin tables and wp_option data 53 | * 54 | * @since 0.0.3 55 | */ 56 | public static function uninstall() { 57 | 58 | // sanity checks 59 | if ( ! current_user_can( 'activate_plugins' ) ) { 60 | return; 61 | } 62 | 63 | check_ajax_referer( 'updates' ); 64 | 65 | // good to go 66 | self::remove_plugin_options(); 67 | self::remove_plugin_tables(); 68 | } 69 | 70 | /** 71 | * Removes plugin options from $prefix_options 72 | * 73 | * @since 0.0.3 74 | */ 75 | private static function remove_plugin_options() { 76 | 77 | $prefix = 'wpcable_'; 78 | 79 | $options = array( 80 | $prefix . 'account_details', 81 | $prefix . 'revenue', 82 | $prefix . 'balance', 83 | $prefix . 'average', 84 | $prefix . 'what_to_check', 85 | $prefix . 'transcactions_version' 86 | ); 87 | 88 | foreach( $options as $option ) { 89 | delete_option( $option ); 90 | } 91 | 92 | } 93 | 94 | /* 95 | * Remove plugin generated tables 96 | * 97 | * @since 0.0.3 98 | */ 99 | private static function remove_plugin_tables() { 100 | global $wpdb; 101 | 102 | $prefix = $wpdb->prefix; 103 | 104 | $tables = array( 105 | $prefix . 'codeable_transcactions', 106 | $prefix . 'codeable_amounts', 107 | $prefix . 'codeable_clients', 108 | ); 109 | 110 | $wpdb->query( 'DROP TABLE IF EXISTS ' . implode( ',', $tables ) ); 111 | } 112 | } 113 | 114 | wpcable_uninstall::uninstall(); -------------------------------------------------------------------------------- /assets/js/jquery.matchHeight-min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery-match-height 0.7.0 by @liabru 3 | * http://brm.io/jquery-match-height/ 4 | * License MIT 5 | */ 6 | !function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):"undefined"!=typeof module&&module.exports?module.exports=t(require("jquery")):t(jQuery)}(function(t){var e=-1,o=-1,i=function(t){return parseFloat(t)||0},a=function(e){var o=1,a=t(e),n=null,r=[];return a.each(function(){var e=t(this),a=e.offset().top-i(e.css("margin-top")),s=r.length>0?r[r.length-1]:null;null===s?r.push(e):Math.floor(Math.abs(n-a))<=o?r[r.length-1]=s.add(e):r.push(e),n=a}),r},n=function(e){var o={ 7 | byRow:!0,property:"height",target:null,remove:!1};return"object"==typeof e?t.extend(o,e):("boolean"==typeof e?o.byRow=e:"remove"===e&&(o.remove=!0),o)},r=t.fn.matchHeight=function(e){var o=n(e);if(o.remove){var i=this;return this.css(o.property,""),t.each(r._groups,function(t,e){e.elements=e.elements.not(i)}),this}return this.length<=1&&!o.target?this:(r._groups.push({elements:this,options:o}),r._apply(this,o),this)};r.version="0.7.0",r._groups=[],r._throttle=80,r._maintainScroll=!1,r._beforeUpdate=null, 8 | r._afterUpdate=null,r._rows=a,r._parse=i,r._parseOptions=n,r._apply=function(e,o){var s=n(o),h=t(e),l=[h],c=t(window).scrollTop(),p=t("html").outerHeight(!0),d=h.parents().filter(":hidden");return d.each(function(){var e=t(this);e.data("style-cache",e.attr("style"))}),d.css("display","block"),s.byRow&&!s.target&&(h.each(function(){var e=t(this),o=e.css("display");"inline-block"!==o&&"flex"!==o&&"inline-flex"!==o&&(o="block"),e.data("style-cache",e.attr("style")),e.css({display:o,"padding-top":"0", 9 | "padding-bottom":"0","margin-top":"0","margin-bottom":"0","border-top-width":"0","border-bottom-width":"0",height:"100px",overflow:"hidden"})}),l=a(h),h.each(function(){var e=t(this);e.attr("style",e.data("style-cache")||"")})),t.each(l,function(e,o){var a=t(o),n=0;if(s.target)n=s.target.outerHeight(!1);else{if(s.byRow&&a.length<=1)return void a.css(s.property,"");a.each(function(){var e=t(this),o=e.attr("style"),i=e.css("display");"inline-block"!==i&&"flex"!==i&&"inline-flex"!==i&&(i="block");var a={ 10 | display:i};a[s.property]="",e.css(a),e.outerHeight(!1)>n&&(n=e.outerHeight(!1)),o?e.attr("style",o):e.css("display","")})}a.each(function(){var e=t(this),o=0;s.target&&e.is(s.target)||("border-box"!==e.css("box-sizing")&&(o+=i(e.css("border-top-width"))+i(e.css("border-bottom-width")),o+=i(e.css("padding-top"))+i(e.css("padding-bottom"))),e.css(s.property,n-o+"px"))})}),d.each(function(){var e=t(this);e.attr("style",e.data("style-cache")||null)}),r._maintainScroll&&t(window).scrollTop(c/p*t("html").outerHeight(!0)), 11 | this},r._applyDataApi=function(){var e={};t("[data-match-height], [data-mh]").each(function(){var o=t(this),i=o.attr("data-mh")||o.attr("data-match-height");i in e?e[i]=e[i].add(o):e[i]=o}),t.each(e,function(){this.matchHeight(!0)})};var s=function(e){r._beforeUpdate&&r._beforeUpdate(e,r._groups),t.each(r._groups,function(){r._apply(this.elements,this.options)}),r._afterUpdate&&r._afterUpdate(e,r._groups)};r._update=function(i,a){if(a&&"resize"===a.type){var n=t(window).width();if(n===e)return;e=n; 12 | }i?-1===o&&(o=setTimeout(function(){s(a),o=-1},r._throttle)):s(a)},t(r._applyDataApi),t(window).bind("load",function(t){r._update(!1,t)}),t(window).bind("resize orientationchange",function(t){r._update(!0,t)})}); -------------------------------------------------------------------------------- /functions/admin-tasks.php: -------------------------------------------------------------------------------- 1 | update_task( $task ); 49 | 50 | echo 'OK'; 51 | exit; 52 | } 53 | add_action( 'wp_ajax_wpcable_update_task', 'wpcable_ajax_update_task' ); 54 | 55 | /** 56 | * Ajax handler that returns a full task list in JSON format. 57 | */ 58 | function wpcable_ajax_reload_tasks() { 59 | $wpcable_tasks = new wpcable_tasks(); 60 | 61 | $task_list = $wpcable_tasks->get_tasks(); 62 | echo wp_json_encode( $task_list ); 63 | exit; 64 | } 65 | add_action( 'wp_ajax_wpcable_reload_tasks', 'wpcable_ajax_reload_tasks' ); 66 | 67 | /** 68 | * Render the settings page. 69 | * 70 | * @return void 71 | */ 72 | function codeable_tasks_callback() { 73 | codeable_page_requires_login( __( 'Your tasks', 'wpcable' ) ); 74 | codeable_admin_notices(); 75 | 76 | $color_flags = []; 77 | $color_flags[''] = [ 78 | 'label' => __( 'New', 'wpcable' ), 79 | 'color' => '', 80 | ]; 81 | $color_flags['prio'] = [ 82 | 'label' => __( 'Priority!', 'wpcable' ), 83 | 'color' => '#cc0000', 84 | ]; 85 | $color_flags['completed'] = [ 86 | 'label' => __( 'Won (completed)', 'wpcable' ), 87 | 'color' => '#b39ddb', 88 | ]; 89 | $color_flags['won'] = [ 90 | 'label' => __( 'Won (active)', 'wpcable' ), 91 | 'color' => '#673ab7', 92 | ]; 93 | $color_flags['estimated'] = [ 94 | 'label' => __( 'Estimated', 'wpcable' ), 95 | 'color' => '#9ccc65', 96 | ]; 97 | $color_flags['optimistic'] = [ 98 | 'label' => __( 'Active Rapport', 'wpcable' ), 99 | 'color' => '#00b0ff', 100 | ]; 101 | $color_flags['neutral'] = [ 102 | 'label' => __( 'Normal', 'wpcable' ), 103 | 'color' => '#80d8ff', 104 | ]; 105 | $color_flags['tough'] = [ 106 | 'label' => __( 'Difficult', 'wpcable' ), 107 | 'color' => '#607d8b', 108 | ]; 109 | $color_flags['pessimistic'] = [ 110 | 'label' => __( 'Unresponsive', 'wpcable' ), 111 | 'color' => '#90a4ae', 112 | ]; 113 | $color_flags['lost'] = [ 114 | 'label' => __( 'Mark as lost', 'wpcable' ), 115 | 'color' => '#cfd8dc', 116 | ]; 117 | 118 | $wpcable_tasks = new wpcable_tasks(); 119 | 120 | $task_list = $wpcable_tasks->get_tasks(); 121 | 122 | $admin_task_table_template = apply_filters('wpcable_admin_task_table_template', WPCABLE_TEMPLATE_DIR.'/admin-task-table.php') ; 123 | ob_start(); 124 | require_once $admin_task_table_template; 125 | echo ob_get_clean(); 126 | } 127 | -------------------------------------------------------------------------------- /assets/fonts/raty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 17 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /classes/object_cache.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class wpcable_cache { 18 | 19 | /** 20 | * The unique cache key to use 21 | * 22 | * @since 0.0.6 23 | * @access private 24 | * @var string $cache_key The unique cache key to use. 25 | */ 26 | private $cache_key; 27 | 28 | /** 29 | * The cache iteration key 30 | * 31 | * @since 0.0.6 32 | * @var string ITTR_KEY The cache iteration 33 | */ 34 | const ITTR_KEY = 'wpcable_ittr'; 35 | 36 | /** 37 | * The query containing data we may need to cache 38 | * 39 | * @since 0.0.6 40 | * @access public 41 | * @var mixed $query The query containing data we may need to cache. 42 | */ 43 | private $query; 44 | 45 | /** 46 | * The amount of seconds to store in the cache 47 | * 48 | * @since 0.0.6 49 | * @access public 50 | * @var int $cache_expires The amount of seconds to store in the cache. 51 | */ 52 | public $cache_expires; 53 | 54 | /** 55 | * Initialize the class and set its properties. 56 | * 57 | * @since 0.0.6 58 | * @param string $cache_key The unique cache key to use. 59 | */ 60 | public function __construct( $cache_key = false, $query = false, $cache_expires = 0 ) { 61 | 62 | if ( ! $cache_key ) { 63 | throw new Exception( 'No cache key provided.' ); 64 | } 65 | 66 | if ( $query ) { 67 | $this->query = $query; 68 | } 69 | 70 | if ( $cache_expires ) { 71 | $this->cache_expires = $cache_expires; 72 | } 73 | 74 | $this->cache_key = $cache_key . $this->get_cache_iteration(); 75 | 76 | } 77 | 78 | /** 79 | * Iterates the cache value 80 | * 81 | * @since 0.0.6 82 | * @return int The current cache iteration 83 | */ 84 | private function get_cache_iteration() { 85 | 86 | $iteration = wp_cache_get( self::ITTR_KEY ); 87 | 88 | if ( $iteration === false ) { 89 | wp_cache_set( self::ITTR_KEY, 1 ); 90 | $iteration = 1; 91 | } 92 | 93 | return $iteration; 94 | } 95 | 96 | /** 97 | * Returns the cached value for the provided key 98 | * 99 | * @since 0.0.6 100 | * @return mixed The cache value 101 | */ 102 | public function get() { 103 | return wp_cache_get( $this->cache_key ); 104 | } 105 | 106 | /** 107 | * Set data in the object cache 108 | * 109 | * @since 0.0.6 110 | * @param $data The data to cache 111 | */ 112 | public function set( $data ) { 113 | wp_cache_set( $this->cache_key, $data, null, $this->cache_expires ); 114 | } 115 | 116 | /** 117 | * Flush the cache by incrementing the cache iteration value 118 | */ 119 | public static function flush() { 120 | wp_cache_incr( self::ITTR_KEY ); 121 | } 122 | 123 | /** 124 | * Check the cache for data and prime it if needed 125 | * 126 | * @since 0.0.6 127 | * @return mixed 128 | */ 129 | public function check() { 130 | global $wpdb; 131 | 132 | if ( empty( $this->query ) ) { 133 | throw new Exception( 'You must provide a query in order to prime the cache' ); 134 | } 135 | 136 | $data = $this->get(); 137 | 138 | if ( $data === false ) { 139 | $data = $wpdb->get_results( $this->query, ARRAY_A ); 140 | $this->set( $data ); 141 | } 142 | 143 | return $data; 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /classes/tasks.php: -------------------------------------------------------------------------------- 1 | tables = [ 31 | 'tasks' => $wpdb->prefix . 'codeable_tasks', 32 | 'clients' => $wpdb->prefix . 'codeable_clients', 33 | ]; 34 | } 35 | 36 | /** 37 | * Returns a list of all tasks. 38 | * 39 | * @return array 40 | */ 41 | public function get_tasks() { 42 | $query = " 43 | SELECT 44 | task.*, 45 | client.full_name AS `client_name`, 46 | client.medium AS `avatar` 47 | FROM 48 | `{$this->tables['tasks']}` AS task 49 | INNER JOIN `{$this->tables['clients']}` AS client 50 | ON client.client_id = task.client_id 51 | ORDER BY task.last_activity DESC, task.task_id DESC 52 | "; 53 | 54 | // Check cache. 55 | $cache_key = 'tasks_list'; 56 | $result = $this->check_cache( $cache_key, $query ); 57 | 58 | $result = array_map( [ $this, 'sanitize_task' ], $result ); 59 | 60 | return $result; 61 | } 62 | 63 | /** 64 | * Takes a single DB row as input and sanitizes some values. 65 | * 66 | * @param array $task Raw DB value. 67 | * @return array Sanitized task. 68 | */ 69 | public function sanitize_task( $task ) { 70 | date_default_timezone_set( get_option( 'timezone_string' ) ); 71 | 72 | $task['estimate'] = (bool) $task['estimate']; 73 | $task['hidden'] = (bool) $task['hidden']; 74 | $task['promoted'] = (bool) $task['promoted']; 75 | $task['subscribed'] = (bool) $task['subscribed']; 76 | $task['favored'] = (bool) $task['favored']; 77 | $task['preferred'] = (bool) $task['preferred']; 78 | $task['client_fee'] = (float) $task['client_fee']; 79 | $task['value'] = (float) $task['value']; 80 | $task['value_client'] = (float) $task['value_client']; 81 | $task['last_activity'] = (int) $task['last_activity']; 82 | $task['last_sync'] = (int) $task['last_sync']; 83 | 84 | $task['last_activity_date'] = date_i18n( 85 | get_option( 'date_format' ), 86 | $task['last_activity'] 87 | ); 88 | $task['last_activity_time'] = date_i18n( 89 | get_option( 'time_format' ), 90 | $task['last_activity'], 91 | false 92 | ); 93 | 94 | return apply_filters( 'wpcable_sanitize_task',$task ); 95 | } 96 | 97 | /** 98 | * Update the specified task in the DB. 99 | * 100 | * @param array $task 101 | */ 102 | public function update_task( $task ) { 103 | global $wpdb; 104 | $wpdb->show_errors(); 105 | 106 | if ( ! is_array( $task ) || empty( $task['task_id'] ) ) { 107 | return; 108 | } 109 | 110 | $valid_fields = [ 111 | 'task_id' => '', 112 | 'state' => '', 113 | 'notes' => '', 114 | 'flag' => '', 115 | ]; 116 | 117 | $task = array_intersect_key( $task, $valid_fields ); 118 | 119 | $wpdb->update( 120 | $this->tables['tasks'], 121 | $task, 122 | [ 'task_id' => $task['task_id'] ] 123 | ); 124 | } 125 | 126 | /** 127 | * Checks and sets cached data 128 | * 129 | * @since 0.0.6 130 | * @author Justin Frydman 131 | * 132 | * @param bool $key The unique cache key. 133 | * @param bool $query The query to check. 134 | * 135 | * @return mixed The raw or cached data. 136 | */ 137 | private function check_cache( $key = false, $query = false ) { 138 | $cache = new wpcable_cache( $key, $query ); 139 | return $cache->check(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /functions/admin-settings.php: -------------------------------------------------------------------------------- 1 | Github Updater and if you already have the expertstatsplugin installed, this will show up. You will need to place your key to get updates. 52 | 53 | If you don't have it installed, you can: 54 | 55 | 1. Hit the "Add plugin" tab 56 | 2. Paste in https://github.com/codeablehq/expertstatsplugin/ under URL 57 | 3. Leave the "github" part 58 | 4. Paste in the github key you just generated, 59 | 5. Hit "Install plugin" 60 | 61 | This will give you the normal WP update notification when a new version ships, and its a 1 click update. 62 | 63 | ### Migrating from Old Version 64 | 65 | You may want to truncate these tables, replace wp_ with your database prefix 66 | 67 | ``` 68 | TRUNCATE TABLE wp_codeable_amounts; 69 | TRUNCATE TABLE wp_codeable_clients; 70 | TRUNCATE TABLE wp_codeable_tasks; 71 | TRUNCATE TABLE wp_codeable_transactions; 72 | TRUNCATE TABLE wp_codeable_transcactions; 73 | ``` 74 | 75 | ### Frequently Asked Questions 76 | 77 | *There was a problem fetching remote data from Codeable* 78 | 79 | Be sure that you set PHP timeout to 120 or more on your first fetch or if you have deleted the cached data. 80 | 81 | 82 | ### Screenshots 83 | 84 | [![Money Charts](https://raw.githubusercontent.com/codeablehq/expertstatsplugin/master/screenshot1-money-charts.png?token=ACPiMX4GRnmFUQzH1KXutE40Y23hf5Pjks5Yvq0bwA%3D%3D)]() 85 | 86 | 87 | [![Client Data](https://github.com/codeablehq/expertstatsplugin/raw/master/screenshot2-client-data.png)]() 88 | 89 | [![Client Data Detail Modal ](https://github.com/codeablehq/expertstatsplugin/raw/master/screenshot3-client-detail-modal.png)]() 90 | -------------------------------------------------------------------------------- /assets/js/offline-exporting.js: -------------------------------------------------------------------------------- 1 | /* 2 | Highcharts JS v5.0.7 (2017-01-17) 3 | Client side exporting module 4 | 5 | (c) 2015 Torstein Honsi / Oystein Moseng 6 | 7 | License: www.highcharts.com/license 8 | */ 9 | (function(n){"object"===typeof module&&module.exports?module.exports=n:n(Highcharts)})(function(n){(function(d){function n(a,d){var c=t.getElementsByTagName("head")[0],b=t.createElement("script");b.type="text/javascript";b.src=a;b.onload=d;b.onerror=function(){console.error("Error loading script",a)};c.appendChild(b)}var C=d.merge,e=d.win,r=e.navigator,t=e.document,z=d.each,w=e.URL||e.webkitURL||e,B=/Edge\/|Trident\/|MSIE /.test(r.userAgent),D=/Edge\/\d+/.test(r.userAgent),E=B?150:0;d.CanVGRenderer= 10 | {};d.dataURLtoBlob=function(a){if(e.atob&&e.ArrayBuffer&&e.Uint8Array&&e.Blob&&w.createObjectURL){a=a.match(/data:([^;]*)(;base64)?,([0-9A-Za-z+/]+)/);for(var d=e.atob(a[3]),c=new e.ArrayBuffer(d.length),c=new e.Uint8Array(c),b=0;br.userAgent.indexOf("Chrome");try{if(!d&&0>r.userAgent.toLowerCase().indexOf("firefox"))return w.createObjectURL(new e.Blob([a],{type:"image/svg+xml;charset-utf-16"}))}catch(c){}return"data:image/svg+xml;charset\x3dUTF-8,"+ 12 | encodeURIComponent(a)};d.imageToDataUrl=function(a,d,c,b,u,l,k,m,p){var g=new e.Image,h,f=function(){setTimeout(function(){var e=t.createElement("canvas"),f=e.getContext&&e.getContext("2d"),x;try{if(f){e.height=g.height*b;e.width=g.width*b;f.drawImage(g,0,0,e.width,e.height);try{x=e.toDataURL(d),u(x,d,c,b)}catch(F){h(a,d,c,b)}}else k(a,d,c,b)}finally{p&&p(a,d,c,b)}},E)},q=function(){m(a,d,c,b);p&&p(a,d,c,b)};h=function(){g=new e.Image;h=l;g.crossOrigin="Anonymous";g.onload=f;g.onerror=q;g.src=a}; 13 | g.onload=f;g.onerror=q;g.src=a};d.downloadSVGLocal=function(a,f,c,b){function u(b,a){a=new e.jsPDF("l","pt",[b.width.baseVal.value+2*a,b.height.baseVal.value+2*a]);e.svg2pdf(b,a,{removeInvalid:!0});return a.output("datauristring")}function l(){y.innerHTML=a;var e=y.getElementsByTagName("text"),g,f=y.getElementsByTagName("svg")[0].style;z(e,function(b){z(["font-family","font-size"],function(a){!b.style[a]&&f[a]&&(b.style[a]=f[a])});b.style["font-family"]=b.style["font-family"]&&b.style["font-family"].split(" ").splice(-1); 14 | g=b.getElementsByTagName("title");z(g,function(a){b.removeChild(a)})});e=u(y.firstChild,0);try{d.downloadURL(e,v),b&&b()}catch(G){c()}}var k,m,p=!0,g,h=f.libURL||d.getOptions().exporting.libURL,y=t.createElement("div"),q=f.type||"image/png",v=(f.filename||"chart")+"."+("image/svg+xml"===q?"svg":q.split("/")[1]),A=f.scale||1,h="/"!==h.slice(-1)?h+"/":h;if("image/svg+xml"===q)try{r.msSaveOrOpenBlob?(m=new MSBlobBuilder,m.append(a),k=m.getBlob("image/svg+xml")):k=d.svgToDataUrl(a),d.downloadURL(k,v), 15 | b&&b()}catch(x){c()}else"application/pdf"===q?e.jsPDF&&e.svg2pdf?l():(p=!0,n(h+"jspdf.js",function(){n(h+"svg2pdf.js",function(){l()})})):(k=d.svgToDataUrl(a),g=function(){try{w.revokeObjectURL(k)}catch(x){}},d.imageToDataUrl(k,q,{},A,function(a){try{d.downloadURL(a,v),b&&b()}catch(F){c()}},function(){var f=t.createElement("canvas"),u=f.getContext("2d"),l=a.match(/^]*width\s*=\s*\"?(\d+)\"?[^>]*>/)[1]*A,k=a.match(/^]*height\s*=\s*\"?(\d+)\"?[^>]*>/)[1]*A,m=function(){u.drawSvg(a,0,0, 16 | l,k);try{d.downloadURL(r.msSaveOrOpenBlob?f.msToBlob():f.toDataURL(q),v),b&&b()}catch(H){c()}finally{g()}};f.width=l;f.height=k;e.canvg?m():(p=!0,n(h+"rgbcolor.js",function(){n(h+"canvg.js",function(){m()})}))},c,c,function(){p&&g()}))};d.Chart.prototype.getSVGForLocalExport=function(a,e,c,b){var f=this,l,k=0,m,p,g,h,n,q=function(a,d,c){++k;c.imageElement.setAttributeNS("http://www.w3.org/1999/xlink","href",a);k===l.length&&b(f.sanitizeSVG(m.innerHTML,p))};d.wrap(d.Chart.prototype,"getChartHTML", 17 | function(b){var a=b.apply(this,Array.prototype.slice.call(arguments,1));p=this.options;m=this.container.cloneNode(!0);return a});f.getSVGForExport(a,e);l=m.getElementsByTagName("image");try{if(l.length)for(h=0,n=l.length;h 2 |
3 | 4 | 5 | 6 |

7 |

8 | ', 12 | '' 13 | ); 14 | ?> 15 |

16 | 17 | 18 | 19 | 20 | 25 | 31 | 32 | 33 | 38 | 44 | 45 | 46 |
21 | 24 | 26 | days 27 |

28 | 29 |

30 |
34 | 37 | 39 | pages 40 |

41 | 42 |

43 |
47 | 48 |
49 |

50 |

51 | ', 55 | '' 56 | ); 57 | ?> 58 |

59 | 60 | 61 | 62 | 63 | 68 | 82 | 83 | 84 | 89 | 105 | 106 | 107 |
64 | 67 | 69 | $ 78 |

79 | 80 |

81 |
85 | 88 | 90 | 101 |

102 | 103 |

104 |
108 | 109 |
110 |

111 |

112 |
113 | 114 |

115 | 116 | 117 | 118 | 119 | 120 | 125 | 140 | 141 | 142 | 143 | 148 | 159 | 160 | 161 | 166 | 177 | 178 | 179 | 180 |
121 | 124 | 126 |

127 | ' . $wpcable_email . '' 131 | ); 132 | ?> 133 | 134 |

135 |

136 | 137 | 138 |

139 |
144 | 147 | 149 | 157 |

158 |
162 | 165 | 167 | 175 |

With your password we generate an auth_token, that is saved encrypted in your DB.', 'wpcable' ); ?>

176 |
181 | 182 |
183 | 184 |
185 | 186 |
187 | 188 | 189 | -------------------------------------------------------------------------------- /classes/api_calls.php: -------------------------------------------------------------------------------- 1 | get_auth_token(); 55 | $this->tasks_stop_at_page = (int) get_option( 'wpcable_tasks_stop_at_page', 0 ); 56 | } 57 | 58 | /** 59 | * Generates an auth_token from the given email/password pair. 60 | * 61 | * @param string $email E-Mail address to use for login. 62 | * @param string $password The users password, as entered on app.codeable.com. 63 | * @return void 64 | */ 65 | public function login( $email, $password ) { 66 | $args = [ 67 | 'email' => $email, 68 | 'password' => $password, 69 | ]; 70 | 71 | $this->auth_token = ''; 72 | $url = 'users/login'; 73 | $login_call = $this->request( $url, $args ); 74 | 75 | // Credential error checking. 76 | if ( 77 | isset( $login_call['errors'] ) && 78 | ! empty( $login_call['errors'][0]['message'] ) && 79 | 'Invalid credentials' === $login_call['errors'][0]['message'] 80 | ) { 81 | $redirect_to = codeable_add_message_param( 'error', 'credentials' ); 82 | 83 | wp_safe_redirect( $redirect_to ); 84 | exit; 85 | } 86 | 87 | $this->set_auth_token( $login_call['auth_token'] ); 88 | } 89 | 90 | /** 91 | * Checks, whether we know the auth-token from a previous API call. 92 | * 93 | * @return bool 94 | */ 95 | public function auth_token_known() { 96 | return !! $this->auth_token; 97 | } 98 | 99 | /** 100 | * Encrypt the auth_token and store it in the options table for future usage. 101 | * 102 | * @param string $token The auth_token. 103 | * @return void 104 | */ 105 | private function set_auth_token( $token ) { 106 | $iv = openssl_random_pseudo_bytes( 16 ); 107 | $enc = openssl_encrypt( $token, 'AES-256-CBC', AUTH_SALT, 0, $iv ); 108 | 109 | $full_enc = base64_encode( $iv ) . ':' . base64_encode( $enc ); 110 | 111 | update_option( 'wpcable_auth_token', $full_enc ); 112 | 113 | $this->auth_token = $token; 114 | } 115 | 116 | /** 117 | * Read the encrypted auth_token from the options table and decrypt it. 118 | * 119 | * @return void 120 | */ 121 | private function get_auth_token() { 122 | $this->auth_token = ''; 123 | 124 | $value = get_option( 'wpcable_auth_token' ); 125 | 126 | if ( $value ) { 127 | list ( $iv, $enc ) = explode( ':', $value ); 128 | 129 | $iv = base64_decode( $iv ); 130 | $enc = base64_decode( $enc ); 131 | $token = openssl_decrypt( $enc, 'AES-256-CBC', AUTH_SALT, 0, $iv ); 132 | 133 | $this->auth_token = $token; 134 | } 135 | } 136 | 137 | /** 138 | * Returns the users profile details. 139 | * 140 | * @return array 141 | */ 142 | public function self() { 143 | $url = 'users/me'; 144 | $login_call = $this->request( $url, [], 'get' ); 145 | 146 | unset( $login_call['auth_token'] ); 147 | 148 | return $login_call; 149 | } 150 | 151 | /** 152 | * Get a batch of transactions of the current user. 153 | * 154 | * @param int $page Pagination offset. First $page = 1. 155 | * @return array 156 | */ 157 | public function transactions_page( $page = 1 ) { 158 | $url = 'users/me/transactions'; 159 | $args = [ 'page' => $page ]; 160 | 161 | $transactions = $this->request( $url, $args, 'get' ); 162 | 163 | return $transactions; 164 | } 165 | 166 | /** 167 | * Get a batch of up to 20 tasks of the current user. 168 | * 169 | * @param string $filter Task-Filter, [pending|active|archived|preferred]. 170 | * @param int $page Pagination offset. First page is $page = 1. 171 | * @return array 172 | */ 173 | public function tasks_page( $filter = 'preferred', $page = 1 ) { 174 | if ( 'hidden_tasks' === $filter ) { 175 | $url = 'users/me/hidden_tasks/'; 176 | $num = 50; 177 | } else { 178 | $url = 'users/me/tasks/' . $filter; 179 | $num = 20; 180 | } 181 | 182 | $args = [ 183 | 'page' => $page, 184 | 'per_page' => $num, 185 | ]; 186 | 187 | // Stop at the next page before it does the call 188 | if ( $this->tasks_stop_at_page 189 | && ( $this->tasks_stop_at_page + 1 ) === $page ) { 190 | return []; 191 | } 192 | 193 | $tasks = $this->request( $url, $args, 'get' ); 194 | 195 | return $tasks; 196 | } 197 | 198 | /** 199 | * Set off an API call too api.codeable.com and return the result as array. 200 | * 201 | * @param string $url API endpoint. 202 | * @param array $args Additional URL params or post data. 203 | * @param string $method Request method [GET|POST]. 204 | * @param array $headers Optional HTTP headers. 205 | * @return array 206 | */ 207 | private function request( $url, $args = [], $method = 'POST', $headers = [] ) { 208 | $response_body = false; 209 | 210 | set_time_limit( 300 ); 211 | 212 | $method = strtoupper( $method ); 213 | $request_args = [ 'method' => $method ]; 214 | $url = 'https://api.codeable.io/' . ltrim( $url, '/' ); 215 | 216 | if ( ! empty( $args ) ) { 217 | if ( 'GET' === $method ) { 218 | $url = add_query_arg( $args, $url ); 219 | } else { 220 | $request_args['body'] = $args; 221 | } 222 | } 223 | 224 | $request_args['headers'] = $headers; 225 | 226 | if ( $this->auth_token_known() ) { 227 | $request_args['headers']['Authorization'] = 'Bearer ' . $this->auth_token; 228 | } 229 | 230 | $response = wp_remote_request( $url, $request_args ); 231 | 232 | if ( is_wp_error( $response ) ) { 233 | trigger_error( 234 | sprintf( 235 | 'Request failed with error %1$s: %2$s', 236 | $response->get_error_code(), $response->get_error_message() 237 | ), 238 | E_USER_ERROR 239 | ); 240 | return false; 241 | } 242 | 243 | $response_body = json_decode( $response['body'], true ); 244 | 245 | if( isset( $response['headers'] ) ) { 246 | 247 | $full_header = $response['headers']->getAll(); 248 | if ( isset( $full_header['auth-token'] ) && !empty( $full_header['auth-token'] ) ) { 249 | 250 | $response_body['auth_token'] = $full_header['auth-token']; 251 | } 252 | } 253 | 254 | if ( is_array( $response_body ) && ! empty( $response_body['errors'] ) ) { 255 | if ( false !== array_search( 'Invalid login credentials', $response_body['errors'], true ) ) { 256 | // The auth_token expired or login failed: Clear the token! 257 | // Next time the user visits the settings page, they need to login again. 258 | codeable_api_logout(); 259 | return false; 260 | } 261 | } 262 | 263 | return $response_body; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /classes/clients.php: -------------------------------------------------------------------------------- 1 | tables = array( 20 | 'transactions' => $wpdb->prefix . 'codeable_transcactions', 21 | 'clients' => $wpdb->prefix . 'codeable_clients', 22 | 'amounts' => $wpdb->prefix . 'codeable_amounts', 23 | ); 24 | 25 | } 26 | 27 | public function get_clients( $from_month = '', $from_year = '', $to_month = '', $to_year = '' ) { 28 | 29 | $clients = array(); 30 | 31 | $firstdate = ''; 32 | $lastdate = ''; 33 | 34 | $wpcable_stats = new wpcable_stats(); 35 | 36 | // get first and last task if no date is set 37 | if ( $from_month == '' && $from_year == '' ) { 38 | $get_first_task = $wpcable_stats->get_first_task(); 39 | $firstdate = $get_first_task['dateadded']; 40 | } else { 41 | $firstdate = $from_year . '-' . $from_month . '-01'; 42 | } 43 | if ( $to_month == '' && $to_year == '' ) { 44 | $get_last_task = $wpcable_stats->get_last_task(); 45 | $lastdate = $get_last_task['dateadded']; 46 | } else { 47 | $lastdate = $to_year . '-' . $to_month . '-' . date( 't', strtotime( $to_year . '-' . $to_month . '-01 23:59:59' ) ); 48 | } 49 | 50 | $query = ' 51 | SELECT 52 | * 53 | FROM 54 | ' . $this->tables['transactions'] . ' 55 | LEFT JOIN ' . $this->tables['clients'] . ' 56 | ON 57 | ' . $this->tables['transactions'] . '.client_id = ' . $this->tables['clients'] . '.client_id 58 | LEFT JOIN ' . $this->tables['amounts'] . ' 59 | ON 60 | ' . $this->tables['transactions'] . '.task_ID = ' . $this->tables['amounts'] . ".task_ID 61 | WHERE 62 | `description` = 'task_completion' OR `description` = 'partial_refund' 63 | AND (dateadded BETWEEN '" . $firstdate . "' AND '" . $lastdate . "') 64 | "; 65 | 66 | // check cache 67 | $cache_key = 'clients_' . $firstdate . '_' . $lastdate; 68 | $result = $this->check_cache( $cache_key, $query ); 69 | 70 | // echo '
'.print_r($result, true).'
'; 71 | $client_variance = array(); 72 | 73 | // loop transactions 74 | foreach ( $result as $tr ) { 75 | 76 | // init indexes 77 | if ( ! isset( $clients['clients'][ $tr['client_id'] ]['total_tasks'] ) ) { 78 | $clients['clients'][ $tr['client_id'] ]['total_tasks'] = 0; 79 | } 80 | if ( ! isset( $clients['clients'][ $tr['client_id'] ]['revenue'] ) ) { 81 | $clients['clients'][ $tr['client_id'] ]['revenue'] = 0; 82 | } 83 | if ( ! isset( $clients['clients'][ $tr['client_id'] ]['tasks'] ) ) { 84 | $clients['clients'][ $tr['client_id'] ]['tasks'] = 0; 85 | } 86 | if ( ! isset( $clients['clients'][ $tr['client_id'] ]['total_tasks'] ) ) { 87 | $clients['clients'][ $tr['client_id'] ]['total_tasks'] = 0; 88 | } 89 | if ( ! isset( $clients['clients'][ $tr['client_id'] ]['tasks'] ) ) { 90 | $clients['clients'][ $tr['client_id'] ]['tasks'] = 0; 91 | } 92 | if ( ! isset( $clients['clients'][ $tr['client_id'] ]['subtasks'] ) ) { 93 | $clients['clients'][ $tr['client_id'] ]['subtasks'] = 0; 94 | } 95 | if ( ! isset( $clients['totals']['refunds'] ) ) { 96 | $clients['totals']['refunds'] = 0; 97 | } 98 | if ( ! isset( $clients['totals']['completed'] ) ) { 99 | $clients['totals']['completed'] = 0; 100 | } 101 | if ( ! isset( $clients['totals']['subtasks'] ) ) { 102 | $clients['totals']['subtasks'] = 0; 103 | } 104 | if ( ! isset( $clients['totals']['tasks'] ) ) { 105 | $clients['totals']['tasks'] = 0; 106 | } 107 | 108 | $clients['clients'][ $tr['client_id'] ]['client_id'] = $tr['client_id']; 109 | $clients['clients'][ $tr['client_id'] ]['revenue'] = ( strpos( $tr['description'], 'refund' ) !== false ? $clients['clients'][ $tr['client_id'] ]['revenue'] : $clients['clients'][ $tr['client_id'] ]['revenue'] + $tr['credit_revenue_amount'] ); 110 | $clients['clients'][ $tr['client_id'] ]['full_name'] = $tr['full_name']; 111 | $clients['clients'][ $tr['client_id'] ]['role'] = $tr['role']; 112 | $clients['clients'][ $tr['client_id'] ]['avatar'] = $tr['large']; 113 | $clients['clients'][ $tr['client_id'] ]['total_tasks'] = $clients['clients'][ $tr['client_id'] ]['total_tasks'] + 1; 114 | $clients['clients'][ $tr['client_id'] ]['tasks'] = ( strpos( $tr['task_type'], 'subtask' ) === false ? $clients['clients'][ $tr['client_id'] ]['tasks'] + 1 : $clients['clients'][ $tr['client_id'] ]['tasks'] ); 115 | $clients['clients'][ $tr['client_id'] ]['subtasks'] = ( strpos( $tr['task_type'], 'subtask' ) !== false ? $clients['clients'][ $tr['client_id'] ]['subtasks'] + 1 : $clients['clients'][ $tr['client_id'] ]['subtasks'] ); 116 | $clients['clients'][ $tr['client_id'] ]['last_sign_in_at'] = $tr['last_sign_in_at']; 117 | $clients['clients'][ $tr['client_id'] ]['timezone_offset'] = $tr['timezone_offset']; 118 | 119 | $clients['totals']['refunds'] = ( strpos( $tr['description'], 'refund' ) !== false ? $clients['totals']['refunds'] + 1 : $clients['totals']['refunds'] ); 120 | $clients['totals']['completed'] = ( strpos( $tr['description'], 'refund' ) === false ? $clients['totals']['completed'] + 1 : $clients['totals']['completed'] ); 121 | $clients['totals']['subtasks'] = ( strpos( $tr['task_type'], 'subtask' ) !== false ? $clients['totals']['subtasks'] + 1 : $clients['totals']['subtasks'] ); 122 | $clients['totals']['tasks'] = ( strpos( $tr['task_type'], 'subtask' ) === false ? $clients['totals']['tasks'] + 1 : $clients['totals']['tasks'] ); 123 | 124 | $clients['clients'][ $tr['client_id'] ]['transactions'][] = array( 125 | 'id' => $tr['id'], 126 | 'description' => $tr['description'], 127 | 'dateadded' => $tr['dateadded'], 128 | 'fee_percentage' => $tr['fee_percentage'], 129 | 'fee_amount' => $tr['fee_amount'], 130 | 'task_type' => $tr['task_type'], 131 | 'task_id' => $tr['task_id'], 132 | 'task_title' => $tr['task_title'], 133 | 'parent_task_id' => $tr['parent_task_id'], 134 | 'preferred' => $tr['preferred'], 135 | 'pro' => $tr['pro'], 136 | 'revenue' => $tr['credit_revenue_amount'], 137 | 'is_refund' => ( strpos( $tr['description'], 'refund' ) !== false ? 1 : 0 ), 138 | ); 139 | 140 | // foreach($clients['clients'] as $client) { 141 | // foreach($client['transactions'] as $tra) { 142 | // $client_variance[$client['client_id']][] = $tra['revenue']; 143 | // } 144 | // $clients['clients'][$client['client_id']]['variance'] = $this->standard_deviation($client_variance[$client['client_id']]); 145 | // } 146 | } 147 | 148 | return $clients; 149 | 150 | } 151 | 152 | 153 | public function standard_deviation( $sample ) { 154 | if ( is_array( $sample ) ) { 155 | $mean = array_sum( $sample ) / count( $sample ); 156 | foreach ( $sample as $key => $num ) { 157 | $devs[ $key ] = pow( $num - $mean, 2 ); 158 | } 159 | 160 | return sqrt( array_sum( $devs ) / ( count( $devs ) - 1 ) ); 161 | } 162 | } 163 | 164 | /** 165 | * Checks and sets cached data 166 | * 167 | * @since 0.0.6 168 | * @author Justin Frydman 169 | * 170 | * @param bool $key The unique cache key 171 | * @param bool $query The query to check 172 | * 173 | * @return mixed The raw or cached data 174 | * @throws Exception 175 | */ 176 | private function check_cache( $key = false, $query = false ) { 177 | 178 | $cache = new wpcable_cache( $key, $query ); 179 | return $cache->check(); 180 | 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /assets/js/exporting.js: -------------------------------------------------------------------------------- 1 | /* 2 | Highcharts JS v5.0.7 (2017-01-17) 3 | Exporting module 4 | 5 | (c) 2010-2016 Torstein Honsi 6 | 7 | License: www.highcharts.com/license 8 | */ 9 | (function(h){"object"===typeof module&&module.exports?module.exports=h:h(Highcharts)})(function(h){(function(f){var h=f.defaultOptions,n=f.doc,A=f.Chart,u=f.addEvent,F=f.removeEvent,D=f.fireEvent,q=f.createElement,B=f.discardElement,v=f.css,p=f.merge,C=f.pick,k=f.each,r=f.extend,G=f.isTouchDevice,E=f.win,H=f.Renderer.prototype.symbols;r(h.lang,{printChart:"Print chart",downloadPNG:"Download PNG image",downloadJPEG:"Download JPEG image",downloadPDF:"Download PDF document",downloadSVG:"Download SVG vector image", 10 | contextButtonTitle:"Chart context menu"});h.navigation={buttonOptions:{theme:{},symbolSize:14,symbolX:12.5,symbolY:10.5,align:"right",buttonSpacing:3,height:22,verticalAlign:"top",width:24}};p(!0,h.navigation,{menuStyle:{border:"1px solid #999999",background:"#ffffff",padding:"5px 0"},menuItemStyle:{padding:"0.5em 1em",background:"none",color:"#333333",fontSize:G?"14px":"11px",transition:"background 250ms, color 250ms"},menuItemHoverStyle:{background:"#335cad",color:"#ffffff"},buttonOptions:{symbolFill:"#666666", 11 | symbolStroke:"#666666",symbolStrokeWidth:3,theme:{fill:"#ffffff",stroke:"none",padding:5}}});h.exporting={type:"image/png",url:"https://export.highcharts.com/",printMaxWidth:780,scale:2,buttons:{contextButton:{className:"highcharts-contextbutton",menuClassName:"highcharts-contextmenu",symbol:"menu",_titleKey:"contextButtonTitle",menuItems:[{textKey:"printChart",onclick:function(){this.print()}},{separator:!0},{textKey:"downloadPNG",onclick:function(){this.exportChart()}},{textKey:"downloadJPEG",onclick:function(){this.exportChart({type:"image/jpeg"})}}, 12 | {textKey:"downloadPDF",onclick:function(){this.exportChart({type:"application/pdf"})}},{textKey:"downloadSVG",onclick:function(){this.exportChart({type:"image/svg+xml"})}}]}}};f.post=function(a,c,e){var b;a=q("form",p({method:"post",action:a,enctype:"multipart/form-data"},e),{display:"none"},n.body);for(b in c)q("input",{type:"hidden",name:b,value:c[b]},null,a);a.submit();B(a)};r(A.prototype,{sanitizeSVG:function(a,c){if(c&&c.exporting&&c.exporting.allowHTML){var e=a.match(/<\/svg>(.*?$)/);e&&(e= 13 | '\x3cforeignObject x\x3d"0" y\x3d"0" width\x3d"'+c.chart.width+'" height\x3d"'+c.chart.height+'"\x3e\x3cbody xmlns\x3d"http://www.w3.org/1999/xhtml"\x3e'+e[1]+"\x3c/body\x3e\x3c/foreignObject\x3e",a=a.replace("\x3c/svg\x3e",e+"\x3c/svg\x3e"))}a=a.replace(/zIndex="[^"]+"/g,"").replace(/isShadow="[^"]+"/g,"").replace(/symbolName="[^"]+"/g,"").replace(/jQuery[0-9]+="[^"]+"/g,"").replace(/url\(("|")(\S+)("|")\)/g,"url($2)").replace(/url\([^#]+#/g,"url(#").replace(/.*?$/,"\x3c/svg\x3e").replace(/(fill|stroke)="rgba\(([ 0-9]+,[ 0-9]+,[ 0-9]+),([ 0-9\.]+)\)"/g,'$1\x3d"rgb($2)" $1-opacity\x3d"$3"').replace(/ /g,"\u00a0").replace(/­/g,"\u00ad");return a=a.replace(//g,"\x3c$1title\x3e").replace(/height=([^" ]+)/g,'height\x3d"$1"').replace(/width=([^" ]+)/g,'width\x3d"$1"').replace(/hc-svg-href="([^"]+)">/g,'xlink:href\x3d"$1"/\x3e').replace(/ id=([^" >]+)/g,' id\x3d"$1"').replace(/class=([^" >]+)/g, 15 | 'class\x3d"$1"').replace(/ transform /g," ").replace(/:(path|rect)/g,"$1").replace(/style="([^"]+)"/g,function(a){return a.toLowerCase()})},getChartHTML:function(){return this.container.innerHTML},getSVG:function(a){var c,e,b,w,m,g=p(this.options,a);n.createElementNS||(n.createElementNS=function(a,c){return n.createElement(c)});e=q("div",null,{position:"absolute",top:"-9999em",width:this.chartWidth+"px",height:this.chartHeight+"px"},n.body);b=this.renderTo.style.width;m=this.renderTo.style.height; 16 | b=g.exporting.sourceWidth||g.chart.width||/px$/.test(b)&&parseInt(b,10)||600;m=g.exporting.sourceHeight||g.chart.height||/px$/.test(m)&&parseInt(m,10)||400;r(g.chart,{animation:!1,renderTo:e,forExport:!0,renderer:"SVGRenderer",width:b,height:m});g.exporting.enabled=!1;delete g.data;g.series=[];k(this.series,function(a){w=p(a.userOptions,{animation:!1,enableMouseTracking:!1,showCheckbox:!1,visible:a.visible});w.isInternal||g.series.push(w)});k(this.axes,function(a){a.userOptions.internalKey=f.uniqueKey()}); 17 | c=new f.Chart(g,this.callback);a&&k(["xAxis","yAxis","series"],function(b){var d={};a[b]&&(d[b]=a[b],c.update(d))});k(this.axes,function(a){var b=f.find(c.axes,function(b){return b.options.internalKey===a.userOptions.internalKey}),d=a.getExtremes(),e=d.userMin,d=d.userMax;!b||void 0===e&&void 0===d||b.setExtremes(e,d,!0,!1)});b=c.getChartHTML();b=this.sanitizeSVG(b,g);g=null;c.destroy();B(e);return b},getSVGForExport:function(a,c){var e=this.options.exporting;return this.getSVG(p({chart:{borderRadius:0}}, 18 | e.chartOptions,c,{exporting:{sourceWidth:a&&a.sourceWidth||e.sourceWidth,sourceHeight:a&&a.sourceHeight||e.sourceHeight}}))},exportChart:function(a,c){c=this.getSVGForExport(a,c);a=p(this.options.exporting,a);f.post(a.url,{filename:a.filename||"chart",type:a.type,width:a.width||0,scale:a.scale,svg:c},a.formAttributes)},print:function(){var a=this,c=a.container,e=[],b=c.parentNode,f=n.body,m=f.childNodes,g=a.options.exporting.printMaxWidth,d,t;if(!a.isPrinting){a.isPrinting=!0;a.pointer.reset(null, 19 | 0);D(a,"beforePrint");if(t=g&&a.chartWidth>g)d=[a.options.chart.width,void 0,!1],a.setSize(g,void 0,!1);k(m,function(a,b){1===a.nodeType&&(e[b]=a.style.display,a.style.display="none")});f.appendChild(c);E.focus();E.print();setTimeout(function(){b.appendChild(c);k(m,function(a,b){1===a.nodeType&&(a.style.display=e[b])});a.isPrinting=!1;t&&a.setSize.apply(a,d);D(a,"afterPrint")},1E3)}},contextMenu:function(a,c,e,b,f,m,g){var d=this,t=d.options.navigation,w=d.chartWidth,h=d.chartHeight,p="cache-"+a, 20 | l=d[p],x=Math.max(f,m),y,z;l||(d[p]=l=q("div",{className:a},{position:"absolute",zIndex:1E3,padding:x+"px"},d.container),y=q("div",{className:"highcharts-menu"},null,l),v(y,r({MozBoxShadow:"3px 3px 10px #888",WebkitBoxShadow:"3px 3px 10px #888",boxShadow:"3px 3px 10px #888"},t.menuStyle)),z=function(){v(l,{display:"none"});g&&g.setState(0);d.openMenu=!1},u(l,"mouseleave",function(){l.hideTimer=setTimeout(z,500)}),u(l,"mouseenter",function(){clearTimeout(l.hideTimer)}),p=u(n,"mouseup",function(b){d.pointer.inClass(b.target, 21 | a)||z()}),u(d,"destroy",p),k(c,function(a){if(a){var b;a.separator?b=q("hr",null,null,y):(b=q("div",{className:"highcharts-menu-item",onclick:function(b){b&&b.stopPropagation();z();a.onclick&&a.onclick.apply(d,arguments)},innerHTML:a.text||d.options.lang[a.textKey]},null,y),b.onmouseover=function(){v(this,t.menuItemHoverStyle)},b.onmouseout=function(){v(this,t.menuItemStyle)},v(b,r({cursor:"pointer"},t.menuItemStyle)));d.exportDivElements.push(b)}}),d.exportDivElements.push(y,l),d.exportMenuWidth= 22 | l.offsetWidth,d.exportMenuHeight=l.offsetHeight);c={display:"block"};e+d.exportMenuWidth>w?c.right=w-e-f-x+"px":c.left=e-x+"px";b+m+d.exportMenuHeight>h&&"top"!==g.alignOptions.verticalAlign?c.bottom=h-b-x+"px":c.top=b+m-x+"px";v(l,c);d.openMenu=!0},addButton:function(a){var c=this,e=c.renderer,b=p(c.options.navigation.buttonOptions,a),f=b.onclick,m=b.menuItems,g,d,h=b.symbolSize||12;c.btnCount||(c.btnCount=0);c.exportDivElements||(c.exportDivElements=[],c.exportSVGElements=[]);if(!1!==b.enabled){var k= 23 | b.theme,n=k.states,q=n&&n.hover,n=n&&n.select,l;delete k.states;f?l=function(a){a.stopPropagation();f.call(c,a)}:m&&(l=function(){c.contextMenu(d.menuClassName,m,d.translateX,d.translateY,d.width,d.height,d);d.setState(2)});b.text&&b.symbol?k.paddingLeft=C(k.paddingLeft,25):b.text||r(k,{width:b.width,height:b.height,padding:0});d=e.button(b.text,0,0,l,k,q,n).addClass(a.className).attr({"stroke-linecap":"round",title:c.options.lang[b._titleKey],zIndex:3});d.menuClassName=a.menuClassName||"highcharts-menu-"+ 24 | c.btnCount++;b.symbol&&(g=e.symbol(b.symbol,b.symbolX-h/2,b.symbolY-h/2,h,h).addClass("highcharts-button-symbol").attr({zIndex:1}).add(d),g.attr({stroke:b.symbolStroke,fill:b.symbolFill,"stroke-width":b.symbolStrokeWidth||1}));d.add().align(r(b,{width:d.width,x:C(b.x,c.buttonOffset)}),!0,"spacingBox");c.buttonOffset+=(d.width+b.buttonSpacing)*("right"===b.align?-1:1);c.exportSVGElements.push(d,g)}},destroyExport:function(a){var c=a?a.target:this;a=c.exportSVGElements;var e=c.exportDivElements;a&& 25 | (k(a,function(a,e){a&&(a.onclick=a.ontouchstart=null,c.exportSVGElements[e]=a.destroy())}),a.length=0);e&&(k(e,function(a,e){clearTimeout(a.hideTimer);F(a,"mouseleave");c.exportDivElements[e]=a.onmouseout=a.onmouseover=a.ontouchstart=a.onclick=null;B(a)}),e.length=0)}});H.menu=function(a,c,e,b){return["M",a,c+2.5,"L",a+e,c+2.5,"M",a,c+b/2+.5,"L",a+e,c+b/2+.5,"M",a,c+b-1.5,"L",a+e,c+b-1.5]};A.prototype.renderExporting=function(){var a,c=this.options.exporting,e=c.buttons,b=this.isDirtyExporting||!this.exportSVGElements; 26 | this.buttonOffset=0;this.isDirtyExporting&&this.destroyExport();if(b&&!1!==c.enabled){for(a in e)this.addButton(e[a]);this.isDirtyExporting=!1}u(this,"destroy",this.destroyExport)};A.prototype.callbacks.push(function(a){a.renderExporting();u(a,"redraw",a.renderExporting);k(["exporting","navigation"],function(c){a[c]={update:function(e,b){a.isDirtyExporting=!0;p(!0,a.options[c],e);C(b,!0)&&a.redraw()}}})})})(h)}); 27 | -------------------------------------------------------------------------------- /templates/admin-task-table.php: -------------------------------------------------------------------------------- 1 |
2 |

8 | 9 |

10 | 66 | 69 |
70 |
71 | 75 | 79 | 83 | 87 | 91 |
92 |
93 | Hide 94 | $info ) { 96 | printf( 97 | '', 98 | esc_attr( $flag ), 99 | esc_html( $info['label'] ) 100 | ); 101 | } 102 | ?> 103 |
104 |
105 | 106 | 107 | 108 | 109 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
110 | 111 | 112 | 113 | 114 |
123 | 133 | 134 |
135 | 233 | 244 | -------------------------------------------------------------------------------- /templates/admin-estimate.php: -------------------------------------------------------------------------------- 1 |
2 |

PERT Estimator

3 |
4 |
5 |
6 |

7 | Time required to complete task 8 |

9 |
10 |
11 | Optimistic estimate: hours 12 | (the lucky case, no scope changes, ...) 13 |
14 |
15 | Most likely estimate: hours 16 | (your experience) 17 |
18 |
19 | Pessimistic estimate: hours 20 | (scope changes, bad communication, technical issues, ...) 21 |
22 |
23 |
24 | 25 |
26 |

27 | 28 | Rate and Fees 29 | 30 | 31 |

32 |
33 |
34 | Your hourly rate: $ 35 |
36 |
37 |

38 | Fees: 39 | .
48 | Following values are used to calculate the total estimate and your earnings. 49 |

50 |
51 |
52 | Contractor fee: % (your fee) 53 |
54 |
55 | Client fee: % (depends on the client) 56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 |

64 | Totals 65 |

66 |
67 |

68 | Take these metrics as consideration if you put more weight on the realistic value. (Proper documentation, clear scope etc.)

69 |
70 | Standard Estimate: hours 71 |
72 |
73 | Paid by the client:
(including fees)
$ 74 |
75 |
76 | Estimate:
(what you enter in Codeable)
$ 77 |
78 |
79 | Your earnings $ 80 |
81 |
82 |
83 | 84 |
85 |

86 | Totals, with extra buffer 87 |

88 |
89 |

90 | Take these metrics as consideration if you put more weight on the pessimistic value. (Not proper documentation, not so clear scope etc.)

91 |
92 | Cautious Estimate: hours 93 |
94 |
95 | Paid by the client:
(including fees)
$ 96 |
97 |
98 | Estimate:
(what you enter in Codeable)
$ 99 |
100 |
101 | Your earnings $ 102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /assets/css/simplemde.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * simplemde v1.11.2 3 | * Copyright Next Step Webs, Inc. 4 | * @link https://github.com/NextStepWebs/simplemde-markdown-editor 5 | * @license MIT 6 | */ 7 | .CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:none;font-variant-ligatures:none}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;min-height:300px;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-scroll{min-height:300px}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)} 8 | -------------------------------------------------------------------------------- /functions/helpers.php: -------------------------------------------------------------------------------- 1 | = $to ? 'increase' : 'decrease'; 14 | } 15 | 16 | function wpcable_date( $value ) { 17 | if ( is_string( $value ) ) { 18 | $time = strtotime( $value ); 19 | } elseif ( is_numeric( $value ) ) { 20 | $time = (int) $value; 21 | } else { 22 | return false; 23 | } 24 | 25 | $format_string = get_option( 'date_format' ) . ' (' . get_option( 'time_format' ) . ')'; 26 | return date_i18n( $format_string, $time ); 27 | } 28 | 29 | /** 30 | * Prepares an URL for a redirect to show certain messages. 31 | * 32 | * @param string $type Either [error|success]. 33 | * @param mixed $values The values to send (a single string or array of strings). 34 | * @param string $url Optionally a different target URL. 35 | * @return string Full URL containing the custom query params. 36 | */ 37 | function codeable_add_message_param( $type, $values, $url = '' ) { 38 | if ( ! $url ) { 39 | $url = admin_url( 'admin.php?page=codeable_settings' ); 40 | } else { 41 | $url = remove_query_arg( [ '_wpnonce', 'action' ], $url ); 42 | } 43 | 44 | if ( 'error' !== $type ) { 45 | $type = 'success'; 46 | } 47 | if ( is_scalar( $values ) ) { 48 | $values = [ $values ]; 49 | } 50 | 51 | $values = array_merge( $values, codeable_get_message_param( $type, $url ) ); 52 | $values = array_unique( $values ); 53 | $values_enc = array_map( 'urlencode', $values ); 54 | $values_enc = base64_encode( implode( ',', $values_enc ) ); 55 | 56 | return add_query_arg( $type, $values_enc, $url ); 57 | } 58 | 59 | /** 60 | * Returns the required message params from the given URL (or current URL, when no 61 | * URL is specified in params) 62 | * 63 | * @param string $type Which messages to return [error|success]. 64 | * @param string $url Optionally a custom URL to parse. 65 | * @return array List of message params. 66 | */ 67 | function codeable_get_message_param( $type, $url = '' ) { 68 | $values = []; 69 | 70 | if ( $url ) { 71 | $parts = parse_url( $url ); 72 | parse_str( $parts['query'], $params ); 73 | } else { 74 | $params = $_REQUEST; 75 | } 76 | 77 | if ( 'error' !== $type ) { 78 | $type = 'success'; 79 | } 80 | 81 | if ( isset( $params[ $type ] ) ) { 82 | $values = base64_decode( $params[ $type ] ); 83 | 84 | if ( $values ) { 85 | $values = explode( ',', $values ); 86 | $values = array_map( 'urldecode', $values ); 87 | } 88 | } 89 | 90 | return is_array( $values ) ? $values : []; 91 | } 92 | 93 | /** 94 | * Check if an SSL warning should be displayed. 95 | * 96 | * @return bool 97 | */ 98 | function codeable_ssl_warning() { 99 | $check = 'auto'; 100 | if ( defined( 'CODEABLE_SSL_CHECK' ) ) { 101 | $check = CODEABLE_SSL_CHECK; 102 | } 103 | 104 | if ( 'off' === $check ) { 105 | return false; 106 | } 107 | 108 | if ( 'auto' === $check ) { 109 | if ( '127.0.0.1' === $_SERVER['REMOTE_ADDR'] ) { 110 | // "Remote" server is on local machine, i.e. development server. 111 | return false; 112 | } elseif ( preg_match( '/\.local$/', $_SERVER['HTTP_HOST'] ) ) { 113 | // A ".local" domain, i.e. development server. 114 | return false; 115 | } 116 | } 117 | 118 | return ! is_ssl(); 119 | } 120 | 121 | /** 122 | * @return bool 123 | * @deprecated 124 | * Check if an PHP Timout warning should be displayed. 125 | * 126 | */ 127 | function codeable_timeout_warning() { 128 | $timeout = ini_get( 'max_execution_time' ); 129 | 130 | if ( $timeout < 120 ) { 131 | return true; 132 | } 133 | 134 | return false; 135 | } 136 | 137 | /** 138 | * Outputs default notices on all Codeable Stats pages. 139 | * 140 | * @return void 141 | */ 142 | function codeable_admin_notices() { 143 | $errors = codeable_get_message_param( 'error' ); 144 | $success = codeable_get_message_param( 'success' ); 145 | 146 | if ( $errors ) : 147 | ?> 148 |
149 | 150 |

151 | 152 |

', $errors ); ?>

153 | 154 |
155 | 160 |
161 | 162 |

163 | 164 |

', $success ); ?>

165 | 166 |
167 | 172 |
173 |

174 |
175 | 188 |
189 |

190 | 191 |
192 |

193 | ', 197 | '' 198 | ); 199 | ?> 200 |

201 |
202 |
203 | prefix . 'codeable_transcactions' => __( 'Transcactions', 'wpcable' ), 231 | $wpdb->prefix . 'codeable_clients' => __( 'Clients', 'wpcable' ), 232 | $wpdb->prefix . 'codeable_amounts' => __( 'Amounts', 'wpcable' ), 233 | $wpdb->prefix . 'codeable_tasks' => __( 'Tasks', 'wpcable' ), 234 | ]; 235 | 236 | $redirect_to = ''; 237 | 238 | foreach ( $tables as $db_table => $db_label ) { 239 | if ( true === $wpdb->query( "TRUNCATE `{$db_table}`;" ) ) { 240 | $redirect_to = codeable_add_message_param( 241 | 'success', 242 | sprintf( 243 | __( 'All %s removed.', 'wpcable' ), 244 | '' . esc_html( $db_label ) . '' 245 | ), 246 | $redirect_to 247 | ); 248 | } else { 249 | $redirect_to = codeable_add_message_param( 250 | 'error', 251 | sprintf( 252 | __( '%s could not be removed.', 'wpcable' ), 253 | '' . esc_html( $db_label ) . '' 254 | ), 255 | $redirect_to 256 | ); 257 | } 258 | } 259 | 260 | // Flush object cache. 261 | wpcable_cache::flush(); 262 | 263 | delete_option( 'wpcable_email' ); 264 | delete_option( 'wpcable_average' ); 265 | delete_option( 'wpcable_balance' ); 266 | delete_option( 'wpcable_revenue' ); 267 | delete_option( 'wpcable_last_fetch' ); 268 | delete_option( 'wpcable_account_details' ); 269 | 270 | codeable_api_logout(); 271 | 272 | $redirect_to = codeable_add_message_param( 273 | 'success', 274 | sprintf( 275 | __( 'Forgot your authentication and profile details.', 'wpcable' ), 276 | esc_html( $db_label ) 277 | ), 278 | $redirect_to 279 | ); 280 | 281 | wp_safe_redirect( $redirect_to ); 282 | exit; 283 | } 284 | 285 | /** 286 | * Checks, whether we should (auto) refresh the stats/pull data from API. 287 | * 288 | * @return void 289 | */ 290 | function codeable_maybe_refresh_data( $force = false ) { 291 | $sync_now = $force; 292 | 293 | if ( ! $sync_now ) { 294 | $last_fetch = (int) get_option( 'wpcable_last_fetch' ); 295 | 296 | if ( ! $last_fetch ) { 297 | $sync_now = true; 298 | } else { 299 | $sync_now = time() - $last_fetch > HOUR_IN_SECONDS; 300 | } 301 | } 302 | 303 | if ( ! $sync_now ) { 304 | return; 305 | } 306 | 307 | $queue = get_option( 'wpcable_api_queue' ); 308 | $data = new wpcable_api_data(); 309 | 310 | if ( empty( $queue ) || ! is_array( $queue ) ) { 311 | $queue = $data->prepare_queue(); 312 | update_option( 'wpcable_api_queue', $queue ); 313 | } 314 | } 315 | 316 | /** 317 | * Display a notice fo the last API fetch and a refresh-button. 318 | * 319 | * @return void 320 | */ 321 | function codeable_last_fetch_info() { 322 | $last_fetch = get_option( 'wpcable_last_fetch' ); 323 | 324 | ?> 325 |
326 | 327 | 328 | 329 | 330 | 331 | 332 | | 333 | 334 | 335 | 336 |
337 | 341 | auth_token_known(); 359 | } 360 | 361 | /** 362 | * Authenticate the user with given email/password and store the auth_token for 363 | * later usage in the DB (encrypted). 364 | * 365 | * @param string $email Users email address. 366 | * @param string $password Password for authentication. 367 | * @return void 368 | */ 369 | function codeable_api_authenticate( $email, $password ) { 370 | $api = wpcable_api_calls::inst(); 371 | $api->login( $email, $password ); 372 | 373 | if ( $api->auth_token_known() ) : 374 | ?> 375 |
376 |

377 | 378 |

379 |
380 | 'RUNNING', 82 | 'step' => $task, 83 | ] ); 84 | } 85 | 86 | $queue = $data->prepare_queue(); 87 | update_option( 'wpcable_api_queue', $queue ); 88 | wp_send_json_success( [ 'state' => 'READY' ] ); 89 | } 90 | add_action( 'wp_ajax_wpcable_sync_start', 'wpcable_sync_start' ); 91 | 92 | /** 93 | * Process the next API call and update the DB. When no API call is enqueued this 94 | * Ajax handler will initialize the API queue. 95 | */ 96 | function wpcable_sync_process() { 97 | $queue = get_option( 'wpcable_api_queue' ); 98 | $data = new wpcable_api_data(); 99 | 100 | // Initialize the API queue on first call. 101 | if ( empty( $queue ) || ! is_array( $queue ) ) { 102 | wp_send_json_error( [ 'state' => 'FINISHED' ] ); 103 | } 104 | 105 | // Process the next pending task. 106 | $task = array_shift( $queue ); 107 | $next = $data->process_queue( $task ); 108 | 109 | // Re-Insert partially completed tasks into the queue. 110 | if ( $next ) { 111 | array_unshift( $queue, $next ); 112 | } 113 | 114 | // Store the timestamp of last full-sync in the options table. 115 | if ( empty( $queue ) ) { 116 | update_option( 'wpcable_last_fetch', time() ); 117 | delete_option( 'wpcable_api_queue', $queue ); 118 | wp_send_json_error( [ 'state' => 'FINISHED' ] ); 119 | } else { 120 | update_option( 'wpcable_api_queue', $queue ); 121 | wp_send_json_error( [ 122 | 'state' => 'RUNNING', 123 | 'step' => $task, 124 | ] ); 125 | } 126 | } 127 | add_action( 'wp_ajax_wpcable_sync_process', 'wpcable_sync_process' ); 128 | 129 | // on install 130 | function wpcable_install() { 131 | global $wpdb; 132 | 133 | $wpcable_db_version = '0.0.3'; 134 | 135 | if ( get_option( 'wpcable_transcactions_version' ) !== $wpcable_db_version ) { 136 | 137 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 138 | 139 | $charset_collate = $wpdb->get_charset_collate(); 140 | 141 | $table_name = $wpdb->prefix . 'codeable_transcactions'; 142 | 143 | $sql = "CREATE TABLE {$table_name} ( 144 | `id` int(11) NOT NULL, 145 | `description` varchar(128) CHARACTER SET utf8 NOT NULL, 146 | `dateadded` datetime NOT NULL, 147 | `fee_percentage` decimal(10,0) DEFAULT NULL, 148 | `fee_amount` decimal(10,0) DEFAULT NULL, 149 | `task_type` varchar(128) CHARACTER SET utf8 DEFAULT NULL, 150 | `task_id` int(11) DEFAULT NULL, 151 | `task_title` text DEFAULT NULL, 152 | `parent_task_id` int(11) DEFAULT NULL, 153 | `preferred` int(4) DEFAULT NULL, 154 | `client_id` int(11) DEFAULT NULL, 155 | `last_sync` int(11) DEFAULT 0 NOT NULL, 156 | PRIMARY KEY (id), 157 | KEY client_id (client_id) 158 | ) $charset_collate;"; 159 | 160 | $db_delta = dbDelta( $sql ); 161 | 162 | $table_name = $wpdb->prefix . 'codeable_amounts'; 163 | 164 | $sql = "CREATE TABLE {$table_name} ( 165 | `task_id` int(11) NOT NULL, 166 | `client_id` int(11) NOT NULL, 167 | `credit_revenue_id` int(11) DEFAULT NULL, 168 | `credit_revenue_amount` int(11) DEFAULT NULL, 169 | `credit_fee_id` int(11) DEFAULT NULL, 170 | `credit_fee_amount` int(11) DEFAULT NULL, 171 | `credit_user_id` int(11) DEFAULT NULL, 172 | `credit_user_amount` int(11) DEFAULT NULL, 173 | `debit_cost_id` int(11) DEFAULT NULL, 174 | `debit_cost_amount` int(11) DEFAULT NULL, 175 | `debit_user_id` int(11) DEFAULT NULL, 176 | `debit_user_amount` int(11) DEFAULT NULL, 177 | PRIMARY KEY (task_id), 178 | KEY client_id (client_id) 179 | ) $charset_collate;"; 180 | 181 | $db_delta = dbDelta( $sql ); 182 | 183 | $table_name = $wpdb->prefix . 'codeable_clients'; 184 | 185 | $sql = "CREATE TABLE {$table_name} ( 186 | `client_id` int(11) NOT NULL, 187 | `full_name` varchar(255) NOT NULL, 188 | `role` varchar(255) DEFAULT NULL, 189 | `last_sign_in_at` datetime DEFAULT NULL, 190 | `pro` int(11) DEFAULT NULL, 191 | `timezone_offset` int(11) DEFAULT NULL, 192 | `tiny` varchar(255) DEFAULT NULL, 193 | `small` varchar(255) DEFAULT NULL, 194 | `medium` varchar(255) DEFAULT NULL, 195 | `large` varchar(255) DEFAULT NULL, 196 | `last_sync` int(11) DEFAULT 0 NOT NULL, 197 | PRIMARY KEY (client_id) 198 | ) $charset_collate;"; 199 | 200 | $db_delta = dbDelta( $sql ); 201 | 202 | $table_name = $wpdb->prefix . 'codeable_tasks'; 203 | 204 | $sql = "CREATE TABLE {$table_name} ( 205 | `task_id` int(11) NOT NULL, 206 | `client_id` int(11) NOT NULL, 207 | `title` varchar(255) NOT NULL, 208 | `estimate` bit DEFAULT 0 NOT NULL, 209 | `hidden` bit DEFAULT 0 NOT NULL, 210 | `promoted` bit DEFAULT 0 NOT NULL, 211 | `subscribed` bit DEFAULT 0 NOT NULL, 212 | `favored` bit DEFAULT 0 NOT NULL, 213 | `preferred` bit DEFAULT 0 NOT NULL, 214 | `client_fee` float DEFAULT 17.5 NOT NULL, 215 | `state` varchar(50) DEFAULT '' NOT NULL, 216 | `kind` varchar(50) DEFAULT '' NOT NULL, 217 | `value` float DEFAULT 0 NOT NULL, 218 | `value_client` float DEFAULT 0 NOT NULL, 219 | `last_activity` int(11) DEFAULT 0 NOT NULL, 220 | `last_activity_by` varchar(200) DEFAULT '' NOT NULL, 221 | `last_sync` int(11) DEFAULT 0 NOT NULL, 222 | `flag` varchar(20) DEFAULT '' NOT NULL, 223 | `notes` text DEFAULT '' NOT NULL, 224 | PRIMARY KEY (task_id) 225 | ) $charset_collate;"; 226 | 227 | $db_delta = dbDelta( $sql ); 228 | 229 | update_option( 'wpcable_transcactions_version', $wpcable_db_version ); 230 | } 231 | } 232 | 233 | register_activation_hook( __FILE__, 'wpcable_install' ); 234 | 235 | // on deactivation 236 | function wpcable_deactivate() { 237 | require_once plugin_dir_path( __FILE__ ) . 'classes/deactivator.php'; 238 | WpCable_deactivator::deactivate(); 239 | 240 | } 241 | 242 | register_deactivation_hook( __FILE__, 'wpcable_deactivate' ); 243 | 244 | function wpcable_admin_scripts( $hook ) { 245 | $plugin_hooks = [ 246 | 'toplevel_page_codeable_transcactions_stats', 247 | 'codeable-stats_page_codeable_tasks', 248 | 'codeable-stats_page_codeable_estimate', 249 | 'codeable-stats_page_codeable_settings', 250 | ]; 251 | 252 | if ( ! in_array( $hook, $plugin_hooks, true ) ) { 253 | return; 254 | } 255 | 256 | wp_enqueue_style( 257 | 'gridcss', 258 | plugins_url( 'assets/css/grid12.css', __FILE__ ) 259 | ); 260 | wp_enqueue_style( 261 | 'wpcablecss', 262 | plugins_url( 'assets/css/wpcable.css', __FILE__ ) 263 | ); 264 | wp_enqueue_style( 265 | 'ratycss', 266 | plugins_url( 'assets/css/jquery.raty.css', __FILE__ ) 267 | ); 268 | wp_enqueue_style( 269 | 'datatablecss', 270 | plugins_url( 'assets/css/jquery.dataTables.min.css', __FILE__ ) 271 | ); 272 | wp_enqueue_style( 273 | 'simplemde', 274 | plugins_url( 'assets/css/simplemde.min.css', __FILE__ ) 275 | ); 276 | 277 | wp_enqueue_script( 278 | 'highchartsjs', 279 | plugins_url( 'assets/js/highcharts.js', __FILE__ ), 280 | [ 'jquery', 'jquery-ui-core', 'jquery-ui-datepicker' ], 281 | null, 282 | true 283 | ); 284 | wp_enqueue_script( 285 | 'highcharts_export_js', 286 | plugins_url( 'assets/js/exporting.js', __FILE__ ), 287 | [ 'jquery', 'highchartsjs' ], 288 | null, 289 | true 290 | ); 291 | wp_enqueue_script( 292 | 'highcharts_offline_export_js', 293 | plugins_url( 'assets/js/offline-exporting.js', __FILE__ ), 294 | [ 'jquery', 'highcharts_export_js' ], 295 | null, 296 | true 297 | ); 298 | wp_enqueue_style( 'jquery-ui-datepicker' ); 299 | 300 | wp_enqueue_script( 301 | 'highcharts3djs', 302 | plugins_url( 'assets/js/highcharts-3d.js', __FILE__ ), 303 | [ 'highchartsjs' ] 304 | ); 305 | wp_enqueue_script( 306 | 'ratyjs', 307 | plugins_url( 'assets/js/jquery.raty.js', __FILE__ ) 308 | ); 309 | wp_enqueue_script( 310 | 'datatablesjs', 311 | plugins_url( 'assets/js/jquery.dataTables.min.js', __FILE__ ) 312 | ); 313 | wp_enqueue_script( 314 | 'matchheightjs', 315 | plugins_url( 'assets/js/jquery.matchHeight-min.js', __FILE__ ) 316 | ); 317 | wp_enqueue_script( 318 | 'simplemde', 319 | plugins_url( 'assets/js/simplemde.min.js', __FILE__ ) 320 | ); 321 | wp_enqueue_script( 322 | 'wpcablejs', 323 | plugins_url( 'assets/js/wpcable.js', __FILE__ ), 324 | [ 'wp-util' ] 325 | ); 326 | } 327 | 328 | add_action( 'admin_enqueue_scripts', 'wpcable_admin_scripts' ); 329 | 330 | add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'wpcable_action_links' ); 331 | 332 | function wpcable_action_links( $links ) { 333 | $link = sprintf( 334 | 'Settings', 335 | esc_url( get_admin_url( null, 'admin.php?page=codeable_settings' ) ) 336 | ); 337 | 338 | array_unshift( $links, $link ); 339 | 340 | return $links; 341 | } 342 | -------------------------------------------------------------------------------- /assets/css/jquery.dataTables.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc{cursor:pointer;*cursor:hand}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("../images/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("../images/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("../images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("../images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("../images/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{-webkit-box-sizing:content-box;box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table,.dataTables_wrapper.no-footer div.dataTables_scrollBody table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}} 2 | -------------------------------------------------------------------------------- /classes/api_data.php: -------------------------------------------------------------------------------- 1 | tables = [ 41 | 'transcactions' => $wpdb->prefix . 'codeable_transcactions', 42 | 'clients' => $wpdb->prefix . 'codeable_clients', 43 | 'amounts' => $wpdb->prefix . 'codeable_amounts', 44 | 'tasks' => $wpdb->prefix . 'codeable_tasks', 45 | ]; 46 | 47 | $this->api_calls = wpcable_api_calls::inst(); 48 | 49 | $this->debug = defined( 'WP_DEBUG' ) ? WP_DEBUG : false; 50 | } 51 | 52 | /** 53 | * Returns a list of API functions that should be called in sequence. 54 | * 55 | * @return array 56 | */ 57 | public function prepare_queue() { 58 | $queue = []; 59 | 60 | $queue[] = [ 61 | 'task' => 'profile', 62 | 'label' => 'User profile', 63 | 'page' => 0, 64 | 'paged' => false, 65 | ]; 66 | $queue[] = [ 67 | 'task' => 'transactions', 68 | 'label' => 'Transactions', 69 | 'page' => 1, 70 | 'paged' => true, 71 | ]; 72 | $queue[] = [ 73 | 'task' => 'task:lost', 74 | 'label' => 'Tasks (lost)', 75 | 'page' => 0, 76 | 'paged' => false, 77 | ]; 78 | $queue[] = [ 79 | 'task' => 'task:pending', 80 | 'label' => 'Tasks (pending)', 81 | 'page' => 1, 82 | 'paged' => true, 83 | ]; 84 | $queue[] = [ 85 | 'task' => 'task:active', 86 | 'label' => 'Tasks (active)', 87 | 'page' => 1, 88 | 'paged' => true, 89 | ]; 90 | $queue[] = [ 91 | 'task' => 'task:preferred', 92 | 'label' => 'Tasks (preferred)', 93 | 'page' => 1, 94 | 'paged' => true, 95 | ]; 96 | $queue[] = [ 97 | 'task' => 'task:in-progress', 98 | 'label' => 'Tasks (in progress)', 99 | 'page' => 1, 100 | 'paged' => true, 101 | ]; 102 | $queue[] = [ 103 | 'task' => 'task:favourites', 104 | 'label' => 'Tasks (favourites)', 105 | 'page' => 1, 106 | 'paged' => true, 107 | ]; 108 | $queue[] = [ 109 | 'task' => 'task:promoted', 110 | 'label' => 'Tasks (promoted)', 111 | 'page' => 1, 112 | 'paged' => false, 113 | ]; 114 | $queue[] = [ 115 | 'task' => 'task:hidden', 116 | 'label' => 'Tasks (hidden)', 117 | 'page' => 1, 118 | 'paged' => true, 119 | ]; 120 | $queue[] = [ 121 | 'task' => 'task:archived', 122 | 'label' => 'Tasks (archived)', 123 | 'page' => 1, 124 | 'paged' => true, 125 | ]; 126 | 127 | return $queue; 128 | } 129 | 130 | 131 | /** 132 | * Processes a single API task and returns the updated task details or false, 133 | * when the task is completed. 134 | * 135 | * @param array $item The task details (generated by prepare_queue above). 136 | * @return array|false 137 | */ 138 | public function process_queue( $item ) { 139 | $curr_page = max( 1, (int) $item['page'] ); 140 | $next_page = false; 141 | 142 | switch ( $item['task'] ) { 143 | case 'profile': 144 | $this->store_profile(); 145 | break; 146 | 147 | case 'transactions': 148 | $next_page = $this->store_transactions( $curr_page ); 149 | break; 150 | 151 | case 'task:lost': 152 | $this->mark_tasks_lost(); 153 | break; 154 | 155 | case 'task:pending': 156 | $next_page = $this->store_tasks( 'pending', $curr_page ); 157 | break; 158 | 159 | case 'task:active': 160 | $next_page = $this->store_tasks( 'active', $curr_page ); 161 | break; 162 | 163 | case 'task:preferred': 164 | $next_page = $this->store_tasks( 'preferred', $curr_page ); 165 | break; 166 | 167 | case 'task:in-progress': 168 | $next_page = $this->store_tasks( 'in-progress', $curr_page ); 169 | break; 170 | 171 | case 'task:favourites': 172 | $next_page = $this->store_tasks( 'favourites', $curr_page ); 173 | break; 174 | 175 | case 'task:promoted': 176 | $next_page = $this->store_tasks( 'promoted', $curr_page ); 177 | break; 178 | 179 | case 'task:hidden': 180 | $next_page = $this->store_tasks( 'hidden_tasks', $curr_page ); 181 | break; 182 | 183 | case 'task:archived': 184 | $next_page = $this->store_tasks( 'archived', $curr_page ); 185 | break; 186 | } 187 | 188 | if ( $next_page && $item['paged'] ) { 189 | $item['page'] = $next_page; 190 | } else { 191 | $item = false; 192 | } 193 | 194 | wpcable_cache::flush(); 195 | return $item; 196 | } 197 | 198 | /** 199 | * Fetches the profile details AND THE AUTH_TOKEN from codeable. 200 | * 201 | * @return void 202 | */ 203 | private function store_profile() { 204 | codeable_page_requires_login( __( 'API Refresh', 'wpcable' ) ); 205 | 206 | $account_details = $this->api_calls->self(); 207 | 208 | update_option( 'wpcable_account_details', $account_details ); 209 | } 210 | 211 | /** 212 | * Fetch transactions from the API and store them in our custom tables. 213 | * 214 | * @param int $page Which page to fetch. 215 | * @return int Number of he next page, or false when no next page exists. 216 | */ 217 | private function store_transactions( $page ) { 218 | global $wpdb; 219 | 220 | codeable_page_requires_login( __( 'API Refresh', 'wpcable' ) ); 221 | 222 | if ( $this->debug ) { 223 | $wpdb->show_errors(); 224 | } 225 | 226 | $single_page = $this->api_calls->transactions_page( $page ); 227 | 228 | if ( 2 === $page ) { 229 | update_option( 'wpcable_average', $single_page['average_task_size'] ); 230 | update_option( 'wpcable_balance', $single_page['balance'] ); 231 | update_option( 'wpcable_revenue', $single_page['revenue'] ); 232 | } 233 | 234 | if ( empty( $single_page['transactions'] ) ) { 235 | return false; 236 | } else { 237 | 238 | // Get all data to the DB. 239 | foreach ( $single_page['transactions'] as $tr ) { 240 | 241 | // Check if transactions already exists. 242 | $check = $wpdb->get_results( 243 | "SELECT COUNT(1) AS totalrows 244 | FROM `{$this->tables['transcactions']}` 245 | WHERE id = '{$tr['id']}'; 246 | " 247 | ); 248 | 249 | $exists = $check[0]->totalrows > 0; 250 | 251 | $new_tr = [ 252 | 'id' => $tr['id'], 253 | 'description' => $tr['description'], 254 | 'dateadded' => date( 'Y-m-d H:i:s', $tr['timestamp'] ), 255 | 'fee_percentage' => $tr['fee_percentage'], 256 | 'fee_amount' => $tr['fee_amount'], 257 | 'task_type' => $tr['task']['kind'], 258 | 'task_id' => $tr['task']['id'], 259 | 'task_title' => $tr['task']['title'], 260 | 'parent_task_id' => ( $tr['task']['parent_task_id'] > 0 ? $tr['task']['parent_task_id'] : 0 ), 261 | 'preferred' => $tr['task']['current_user_is_preferred_contractor'], 262 | 'client_id' => $tr['task_client']['id'], 263 | 'last_sync' => time(), 264 | ]; 265 | 266 | // the API is returning some blank rows, ensure we have a valid client_id. 267 | if ( $new_tr['id'] && is_int( $new_tr['id'] ) ) { 268 | if ( $exists ) { 269 | $db_res = $wpdb->update( 270 | $this->tables['transcactions'], 271 | $new_tr, 272 | [ 'id' => $tr['id'] ] 273 | ); 274 | } else { 275 | $db_res = $wpdb->insert( 276 | $this->tables['transcactions'], 277 | $new_tr 278 | ); 279 | } 280 | } 281 | 282 | if ( $db_res === false ) { 283 | wp_die( 284 | 'Could not insert transactions ' . 285 | $tr['id'] . ':' . 286 | $wpdb->print_error() 287 | ); 288 | } 289 | 290 | $this->store_client( $tr['task_client'] ); 291 | $this->store_amount( 292 | $tr['task']['id'], 293 | $tr['task_client']['id'], 294 | $tr['credit_amounts'], 295 | $tr['debit_amounts'] 296 | ); 297 | 298 | // If we find a transaction that already exists, bail out and don't continue updating all the transactions 299 | if ( $exists ) { 300 | update_option( 'wpcable_average', $single_page['average_task_size'] ); 301 | update_option( 'wpcable_balance', $single_page['balance'] ); 302 | update_option( 'wpcable_revenue', $single_page['revenue'] ); 303 | return false; 304 | } 305 | 306 | } 307 | 308 | return $page + 1; 309 | } 310 | } 311 | 312 | /** 313 | * Marks all pending tasks as "lost", since the `store_tasks()` method will only 314 | * receive pending/won tasks. This way we know, that all tasks that were not 315 | * fetched by the `store_tasks()` method are not available for us anymore. 316 | * 317 | * @return void 318 | */ 319 | private function mark_tasks_lost() { 320 | global $wpdb; 321 | 322 | $lost_state = [ 323 | 'state' => 'lost', 324 | 'estimate' => false, 325 | 'hidden' => true, 326 | 'promoted' => false, 327 | 'subscribed' => false, 328 | 'favored' => false, 329 | 'preferred' => false, 330 | ]; 331 | 332 | $wpdb->update( 333 | $this->tables['tasks'], 334 | $lost_state, 335 | [ 'state' => 'published' ] 336 | ); 337 | 338 | $wpdb->update( 339 | $this->tables['tasks'], 340 | $lost_state, 341 | [ 'state' => 'estimated' ] 342 | ); 343 | 344 | $wpdb->update( 345 | $this->tables['tasks'], 346 | $lost_state, 347 | [ 'state' => 'hired' ] 348 | ); 349 | } 350 | 351 | /** 352 | * Fetch tasks from the API and store them in our custom tables. 353 | * 354 | * @param string $filter The task-filter to apply. 355 | * @param int $page The page to load from API. 356 | * @return int Number of he next page, or false when no next page exists. 357 | */ 358 | private function store_tasks( $filter, $page ) { 359 | global $wpdb; 360 | 361 | codeable_page_requires_login( __( 'API Refresh', 'wpcable' ) ); 362 | 363 | if ( $this->debug ) { 364 | $wpdb->show_errors(); 365 | } 366 | 367 | $single_page = $this->api_calls->tasks_page( $filter, $page ); 368 | $cancel_after_hours = 24 * (int) get_option( 'wpcable_cancel_after_days', 180 ); 369 | 370 | if ( empty( $single_page ) ) { 371 | return false; 372 | } else { 373 | 374 | // Get all data to the DB. 375 | foreach ( $single_page as $task ) { 376 | 377 | // Check if the task already exists. 378 | $check = $wpdb->get_results( 379 | "SELECT COUNT(1) AS totalrows 380 | FROM `{$this->tables['tasks']}` 381 | WHERE task_id = '{$task['id']}'; 382 | " 383 | ); 384 | 385 | // If the record exists then continue with next filter. 386 | $exists = $check[0]->totalrows > 0; 387 | 388 | $new_task = [ 389 | 'task_id' => $task['id'], 390 | 'client_id' => $task['client']['id'], 391 | 'title' => $task['title'], 392 | 'estimate' => ! empty( $task['estimatable'] ), 393 | 'hidden' => ! empty( $task['hidden_by_current_user'] ), 394 | 'promoted' => ! empty( $task['promoted_task'] ), 395 | 'subscribed' => ! empty( $task['subscribed_by_current_user'] ), 396 | 'favored' => ! empty( $task['favored_by_current_user'] ), 397 | 'preferred' => ! empty( $task['current_user_is_preferred_contractor'] ), 398 | 'client_fee' => (float) $task['prices']['client_fee_percentage'], 399 | 'state' => $task['state'], 400 | 'kind' => $task['kind'], 401 | 'value' => (float) $task['prices']['contractor_earnings'], 402 | 'value_client' => (float) $task['prices']['client_price_after_discounts'], 403 | 'last_sync' => time(), 404 | ]; 405 | 406 | if ( ! empty( $task['last_event']['object']['timestamp'] ) ) { 407 | $new_task['last_activity'] = (int) $task['last_event']['object']['timestamp']; 408 | $new_task['last_activity_by'] = ''; 409 | } elseif ( ! empty( $task['last_event']['object']['published_at'] ) ) { 410 | $new_task['last_activity'] = (int) $task['last_event']['object']['published_at']; 411 | $new_task['last_activity_by'] = ''; 412 | }elseif ( ! empty( $task['last_event']['object']['created_at'] ) ) { 413 | $new_task['last_activity'] = (int) strtotime($task['last_event']['object']['created_at']); 414 | $new_task['last_activity_by'] = ''; 415 | } 416 | elseif ( ! empty( $task['last_event']['object']['updated_at'] ) ) { 417 | $new_task['last_activity'] = (int) strtotime($task['last_event']['object']['updated_at']); 418 | $new_task['last_activity_by'] = ''; 419 | } 420 | 421 | 422 | if ( ! empty( $task['last_event']['user']['full_name'] ) ) { 423 | $new_task['last_activity_by'] = $task['last_event']['user']['full_name']; 424 | } 425 | 426 | if ( ! empty( $task['last_event']['type']) && 'create_vault' === $task['last_event']['type']) { 427 | $new_task['last_activity'] = (int) strtotime($task['last_event']['user']['last_sign_in_at']); 428 | $new_task['last_activity_by'] = $task['last_event']['user']['full_name']; 429 | } 430 | 431 | // Some simple rules to automatically detect the correct flag for tasks. 432 | if ( 'canceled' === $task['state'] ) { 433 | // Tasks that were canceled by the client obviously are lost. 434 | $new_task['flag'] = 'lost'; 435 | $new_task['last_activity'] = (int) strtotime( $task['published_at'] ); 436 | } elseif ( $new_task['hidden'] ) { 437 | // Tasks that I hide from my Codeable list are "lost for us". 438 | $new_task['flag'] = 'lost'; 439 | } elseif ( ! empty( $new_task['last_activity'] ) ) { 440 | // This means that the workroom is public or private for me. 441 | if ( 'completed' === $task['state'] ) { 442 | $new_task['flag'] = 'completed'; 443 | } 444 | if ( 'paid' === $task['state'] ) { 445 | $new_task['flag'] = 'won'; 446 | } 447 | if ( 'hired' === $task['state'] ) { 448 | $new_task['flag'] = 'estimated'; 449 | } 450 | } elseif ( empty( $new_task['last_activity'] ) ) { 451 | // This workroom is private for another expert = possibly lost. 452 | if ( in_array( $task['state'], [ 'hired', 'completed', 'refunded', 'paid' ], true ) ) { 453 | $new_task['flag'] = 'lost'; 454 | // Disabled last activity for these ones, as it was causing more problems than solving 455 | // $new_task['last_activity'] = (int) strtotime( $task['published_at'] ); 456 | } 457 | } 458 | 459 | // Flag open tasks as "canceled" after a given number of stale days. 460 | if ( in_array( $task['state'], [ 'published', 'estimated', 'hired' ], true ) ) { 461 | if ( ! empty( $new_task['last_activity'] ) ) { 462 | $stale_hours = floor( 463 | ( time() - $new_task['last_activity'] ) / HOUR_IN_SECONDS 464 | ); 465 | 466 | if ( $stale_hours > $cancel_after_hours ) { 467 | $new_task['flag'] = 'lost'; 468 | } 469 | } 470 | } 471 | 472 | // The API is returning some blank rows, ensure we have a valid id. 473 | if ( $new_task['task_id'] && is_int( $new_task['task_id'] ) ) { 474 | if ( $exists ) { 475 | $db_res = $wpdb->update( 476 | $this->tables['tasks'], 477 | $new_task, 478 | [ 'task_id' => $task['id'] ] 479 | ); 480 | } else { 481 | $db_res = $wpdb->insert( 482 | $this->tables['tasks'], 483 | $new_task 484 | ); 485 | } 486 | } 487 | 488 | if ( $db_res === false ) { 489 | wp_die( 490 | 'Could not insert task ' . 491 | $task['id'] . ':' . 492 | $wpdb->print_error() 493 | ); 494 | } 495 | 496 | $this->store_client( $task['client'] ); 497 | } 498 | 499 | return $page + 1; 500 | } 501 | } 502 | 503 | /** 504 | * Insert new clients to the clients-table. 505 | * 506 | * @param array $client Client details. 507 | * @return void 508 | */ 509 | private function store_client( $client ) { 510 | global $wpdb; 511 | 512 | // The API is returning some blank rows, ensure we have a valid client_id. 513 | if ( ! $client || ! is_int( $client['id'] ) ) { 514 | return; 515 | } 516 | 517 | // Check, if the client already exists. 518 | $check_client = $wpdb->get_results( 519 | "SELECT COUNT(1) AS totalrows 520 | FROM `{$this->tables['clients']}` 521 | WHERE client_id = '{$client['id']}';" 522 | ); 523 | 524 | // When the client already exists, stop here. 525 | $exists = $check_client[0]->totalrows > 0; 526 | 527 | $new_client = [ 528 | 'client_id' => $client['id'], 529 | 'full_name' => $client['full_name'], 530 | 'role' => $client['role'], 531 | 'last_sign_in_at' => date( 'Y-m-d H:i:s', strtotime( $client['last_sign_in_at'] ) ), 532 | 'pro' => $client['pro'], 533 | 'timezone_offset' => $client['timezone_offset'], 534 | 'tiny' => $client['avatar']['tiny_url'], 535 | 'small' => $client['avatar']['small_url'], 536 | 'medium' => $client['avatar']['medium_url'], 537 | 'large' => $client['avatar']['large_url'], 538 | 'last_sync' => time(), 539 | ]; 540 | 541 | if ( $exists ) { 542 | $wpdb->update( 543 | $this->tables['clients'], 544 | $new_client, 545 | [ 'client_id' => $client['id'] ] 546 | ); 547 | } else { 548 | $wpdb->insert( $this->tables['clients'], $new_client ); 549 | } 550 | } 551 | 552 | /** 553 | * Insert pricing details into the amounts table. 554 | * 555 | * @param array $client Client details. 556 | * @return void 557 | */ 558 | private function store_amount( $task_id, $client_id, $credit, $debit ) { 559 | global $wpdb; 560 | 561 | // The API is returning some blank rows, ensure we have a valid client_id. 562 | if ( ! $task_id || ! is_int( $task_id ) ) { 563 | return; 564 | } 565 | 566 | $new_amount = [ 567 | 'task_id' => $task_id, 568 | 'client_id' => $client_id, 569 | 'credit_revenue_id' => $credit[0]['id'], 570 | 'credit_revenue_amount' => $credit[0]['amount'], 571 | 'credit_fee_id' => $credit[1]['id'], 572 | 'credit_fee_amount' => $credit[1]['amount'], 573 | 'credit_user_id' => $credit[2]['id'], 574 | 'credit_user_amount' => $credit[2]['amount'], 575 | 'debit_cost_id' => $debit[0]['id'], 576 | 'debit_cost_amount' => $debit[0]['amount'], 577 | 'debit_user_id' => $debit[1]['id'], 578 | 'debit_user_amount' => $debit[1]['amount'], 579 | ]; 580 | 581 | $wpdb->replace( $this->tables['amounts'], $new_amount ); 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /assets/js/wpcable.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function () { 2 | 3 | jQuery('.compareicon').click(function() { 4 | jQuery('.compareto').toggle(); 5 | jQuery('#compare_date_from, #compare_date_to').val(''); 6 | }); 7 | 8 | jQuery('div[data-score]').raty({ 9 | cancel: false, 10 | readOnly: true, 11 | half: true, 12 | starType: 'i', 13 | score: function () { 14 | return jQuery(this).attr('data-score'); 15 | } 16 | }); 17 | 18 | jQuery(function () { 19 | jQuery('.row.match_height .column_inner').matchHeight(); 20 | }); 21 | 22 | var start_year = jQuery('#date_form').attr('data-start-year'); 23 | var end_year = jQuery('#date_form').attr('data-end-year'); 24 | 25 | jQuery('.datepicker').datepicker({ 26 | buttonImage: jQuery('.datepicker').attr('data-icon'), 27 | buttonImageOnly: true, 28 | showOn: "button", 29 | buttonText: "Select date", 30 | maxDate: '0', 31 | changeMonth: true, 32 | changeYear: true, 33 | showButtonPanel: true, 34 | dateFormat: 'yy-mm', 35 | yearRange: '' + start_year + ':' + end_year + '', 36 | onClose: function (dateText, inst) { 37 | jQuery(this).datepicker('setDate', new Date(inst.selectedYear, inst.selectedMonth, 1)); 38 | } 39 | }); 40 | 41 | jQuery('.datatable_inner').each(function () { 42 | jQuery(this).DataTable({ 43 | "order": [[1, "desc"]] 44 | }); 45 | }); 46 | jQuery('#clients_table').DataTable({ 47 | "order": [[7, "desc"]] 48 | }); 49 | 50 | // ----- 51 | // Remove the "error" and "success" params from settings URL after page load. 52 | 53 | var url = window.location.href; 54 | url = setUrlParameter(url, 'error', ''); 55 | url = setUrlParameter(url, 'success', ''); 56 | window.history.replaceState({}, window.document.title, url); 57 | 58 | function setUrlParameter(url, key, value) { 59 | var key = encodeURIComponent(key), 60 | value = encodeURIComponent(value); 61 | 62 | var baseUrl = url.split('?')[0], 63 | newParam = key + '=' + value, 64 | params = '?' + newParam; 65 | 66 | // if there are no query strings, make urlQueryString empty 67 | if (url.split('?')[1] === undefined){ 68 | urlQueryString = ''; 69 | } else { 70 | urlQueryString = '?' + url.split('?')[1]; 71 | } 72 | 73 | // If the "search" string exists, then build params from it 74 | if (urlQueryString) { 75 | var updateRegex = new RegExp('([\?&])' + key + '[^&]*'); 76 | var removeRegex = new RegExp('([\?&])' + key + '=[^&;]+[&;]?'); 77 | 78 | if (typeof value === 'undefined' || value === null || value === '') { 79 | // Remove param if value is empty 80 | params = urlQueryString.replace(removeRegex, "$1"); 81 | params = params.replace(/[&;]$/, ""); 82 | 83 | } else if (urlQueryString.match(updateRegex) !== null) { 84 | // If param exists already, update it 85 | params = urlQueryString.replace(updateRegex, "$1" + newParam); 86 | 87 | } else if (urlQueryString === '') { 88 | // If there are no query strings 89 | params = '?' + newParam; 90 | } else { 91 | // Otherwise, add it to end of query string 92 | params = urlQueryString + '&' + newParam; 93 | } 94 | } 95 | 96 | // no parameter was set so we don't need the question mark 97 | params = params === '?' ? '' : params; 98 | 99 | return baseUrl + params; 100 | } 101 | }); 102 | 103 | /** 104 | * Sync handler. 105 | */ 106 | jQuery(document).ready(function () { 107 | var elProgress = jQuery( '.codeable-sync-progress' ); 108 | 109 | if ( ! elProgress.length ) { 110 | return; 111 | } 112 | 113 | function processNext() { 114 | window.setTimeout(function () { 115 | jQuery.post( 116 | window.ajaxurl, 117 | { 118 | action: 'wpcable_sync_process' 119 | }, 120 | checkSyncStatus 121 | ); 122 | }, 1); 123 | } 124 | 125 | function showProgressBar( step ) { 126 | var label = 'Fetching data'; 127 | 128 | if ( step && step.label ) { 129 | label = step.label; 130 | } 131 | if ( step && ! isNaN( step.page ) && step.paged ) { 132 | label += ', page ' + step.page; 133 | } 134 | 135 | elProgress.show(); 136 | elProgress.find( '.msg' ).text( label ); 137 | } 138 | 139 | function hideProgressBar() { 140 | if ( elProgress.is(':visible') ) { 141 | elProgress.hide(); 142 | 143 | if ( jQuery( '.wrap.wpcable_wrap.tasks' ).length ) { 144 | // On the tasks-page silently refresh the task list without reload. 145 | jQuery( document ).trigger( 'codeable-reload-tasks' ); 146 | } else if ( jQuery( '.wrap.cable_stats_wrap' ).length ) { 147 | // On stats-page we can reload the whole window without a problem. 148 | window.location.reload(); 149 | } 150 | 151 | // On any other page, the refresh has no effect and no action is needed. 152 | } 153 | } 154 | 155 | function checkSyncStatus( res ) { 156 | var state = false; 157 | if ( res && res.data ) { 158 | state = res.data.state 159 | } 160 | 161 | if ( state === 'RUNNING' || state === 'READY' ) { 162 | showProgressBar( res.data.step ); 163 | processNext(); 164 | } else { 165 | hideProgressBar(); 166 | } 167 | } 168 | 169 | function startSync( ev ) { 170 | jQuery.post( 171 | window.ajaxurl, 172 | { 173 | action: 'wpcable_sync_start' 174 | }, 175 | checkSyncStatus 176 | ); 177 | 178 | ev.preventDefault(); 179 | return false; 180 | } 181 | 182 | jQuery( '.sync-start' ).on( 'click', startSync ); 183 | 184 | // Check, if a sync process is active from previous page load. 185 | processNext(); 186 | 187 | // Check for new sync process every 5 minutes. 188 | window.setInterval( processNext, 300000 ); 189 | }); 190 | 191 | /** 192 | * Task list functions. 193 | */ 194 | jQuery(document).ready(function () { 195 | if (!jQuery('.wrap.wpcable_wrap.tasks').length) { 196 | return; 197 | } 198 | if (!window.wpcable || ! window.wpcable.tasks) { 199 | return; 200 | } 201 | 202 | var list = jQuery( '.wrap .task-list' ); 203 | var listTitle = jQuery( '.wrap .list-title' ); 204 | var itemTpl = wp.template( 'list-item' ); 205 | var notesForm = jQuery( '.wrap .notes-editor-layer' ); 206 | var filterCb = jQuery( '.wrap [data-filter]' ); 207 | var flagCb = jQuery( '.wrap [data-flag]' ); 208 | var filterTxt = jQuery( '.wrap #post-search-input' ); 209 | var notesMde = false; 210 | var filterTxtVal = ''; 211 | 212 | var filterState = 'all'; 213 | var currFilters = {}; 214 | var currFlags = {}; 215 | 216 | function reloadData() { 217 | jQuery.post( 218 | window.ajaxurl, 219 | { 220 | action: 'wpcable_reload_tasks' 221 | }, 222 | function ( res ) { 223 | if (! res || ! Array.isArray( res ) ) { 224 | return; 225 | } 226 | window.wpcable.tasks = res; 227 | 228 | initFilters(); 229 | updateFilters(); 230 | refreshList(); 231 | }, 232 | 'json' 233 | ); 234 | } 235 | 236 | function refreshList() { 237 | list.empty(); 238 | 239 | for ( var i = 0; i < wpcable.tasks.length; i++ ) { 240 | var task = wpcable.tasks[i]; 241 | 242 | if ( filterState === 'all' ) { 243 | // Display all tasks. 244 | } else if ( filterState === 'lost' && (task.state === 'lost' || task.flag === 'lost')) { 245 | // Display lost tasks list. 246 | } else if ( filterState === task.state && task.flag !== 'lost' ) { 247 | // Display single task state list. 248 | } else { 249 | continue; 250 | } 251 | if ( false === task._visible ) { 252 | continue; 253 | } 254 | 255 | function _refresh() { 256 | var task = this; 257 | var prev = list.find( '#task-' + task.task_id ); 258 | 259 | task.notes_html = SimpleMDE.prototype.markdown( task.notes ); 260 | 261 | task.$el = jQuery( itemTpl( task ) ); 262 | task.$el.data( 'task', task ); 263 | 264 | if ( prev.length ) { 265 | prev.off( 'task:refresh', _refresh ); 266 | prev.replaceWith( task.$el ); 267 | } else { 268 | list.append( task.$el ); 269 | } 270 | 271 | task.$el.on( 'task:refresh', _refresh ); 272 | } 273 | _refresh = _refresh.bind( task ) 274 | 275 | _refresh(); 276 | } 277 | 278 | updateTitle(); 279 | } 280 | 281 | function updateTitle() { 282 | listTitle.empty(); 283 | 284 | var count = list.find( '.list-item:visible' ).length; 285 | if ( ! count ) { 286 | listTitle.text( listTitle.data('none') ); 287 | } else if ( 1 === count ) { 288 | listTitle.text( listTitle.data('one') ); 289 | } else { 290 | listTitle.text( listTitle.data('many').replace( '[NUM]', count ) ); 291 | } 292 | } 293 | 294 | function updateFilters( ev ) { 295 | var hash = window.location.hash.substr( 1 ).split( '=' ); 296 | var filterRe = false; 297 | 298 | // Update filters based on current #hash value. 299 | if ( hash && 2 === hash.length ) { 300 | if ( 'state' === hash[0] ) { 301 | filterState = hash[1]; 302 | } 303 | } 304 | 305 | filterCb.each(function() { 306 | var el = jQuery(this); 307 | var key = el.data('filter'); 308 | 309 | if ( ! el.prop('checked') ) { 310 | currFilters[key] = false; 311 | } else { 312 | currFilters[key] = true; 313 | } 314 | }); 315 | 316 | currFlags = []; 317 | flagCb.each(function() { 318 | var el = jQuery(this); 319 | var key = el.data('flag'); 320 | 321 | if ( el.prop('checked') ) { 322 | currFlags.push( key ); 323 | } 324 | }); 325 | 326 | filterTxtVal = filterTxt.val(); 327 | 328 | if ( filterTxtVal.length ) { 329 | filterRe = new RegExp( filterTxtVal, 'i' ); 330 | } 331 | 332 | // Mark tasks as visible/hidden. 333 | for ( var i = 0; i < wpcable.tasks.length; i++ ) { 334 | var task = wpcable.tasks[i]; 335 | task._visible = true; 336 | 337 | if ( task.hidden && currFilters.no_hidden ) { 338 | task._visible = false; 339 | } 340 | if ( !task.favored && currFilters.favored ) { 341 | task._visible = false; 342 | } 343 | if ( !task.promoted && currFilters.promoted ) { 344 | task._visible = false; 345 | } 346 | if ( !task.subscribed && currFilters.subscribed ) { 347 | task._visible = false; 348 | } 349 | if ( !task.preferred && currFilters.preferred ) { 350 | task._visible = false; 351 | } 352 | 353 | if ( currFlags.length ) { 354 | if ( -1 !== currFlags.indexOf( task.flag ) ) { 355 | task._visible = false; 356 | } 357 | } 358 | 359 | if (filterRe) { 360 | if ( 361 | -1 === task.title.search( filterRe ) && 362 | -1 === task.task_id.search( filterRe ) && 363 | -1 === task.client_name.search( filterRe ) && 364 | -1 === task.notes.search( filterRe ) 365 | ) { 366 | task._visible = false; 367 | } 368 | } 369 | } 370 | 371 | // Update the UI to reflect active filters. 372 | var currState = jQuery( '.subsubsub li.' + filterState ).filter( ':visible' ); 373 | 374 | jQuery( '.subsubsub .current' ).removeClass( 'current' ) 375 | if ( 1 !== currState.length ) { 376 | filterState = 'all'; 377 | currState = jQuery( '.subsubsub li.all' ); 378 | } 379 | 380 | currState.find( 'a' ).addClass('current'); 381 | refreshList(); 382 | 383 | if ( ev ) { 384 | ev.preventDefault(); 385 | } 386 | return false; 387 | } 388 | 389 | function initFilters() { 390 | var totals = {}; 391 | 392 | totals.all = 0; 393 | for ( var i = 0; i < wpcable.tasks.length; i++ ) { 394 | var task = wpcable.tasks[i]; 395 | var state = task.state; 396 | 397 | if ('lost' === task.flag) { 398 | state = 'lost'; 399 | } 400 | 401 | if ( false === task._visible ) { 402 | continue; 403 | } 404 | 405 | if ( undefined === totals[state] ) { 406 | totals[state] = 0; 407 | } 408 | totals[state]++; 409 | totals.all++; 410 | } 411 | 412 | jQuery( '.subsubsub li' ).hide(); 413 | 414 | for ( var i in totals ) { 415 | var item = jQuery( '.subsubsub li.' + i ); 416 | item.show(); 417 | item.find( '.count' ).text( totals[i] ); 418 | } 419 | 420 | filterState = 'all'; 421 | } 422 | 423 | function startEditor( ev ) { 424 | if ( ! list.isClick ) { 425 | return; 426 | } 427 | if ( notesMde ) { 428 | closeEditor(); 429 | } 430 | 431 | var row = jQuery( this ).closest( 'tr' ); 432 | var task = row.data( 'task' ); 433 | 434 | notesForm.show(); 435 | notesForm.find( '.task-title' ).text( task.title ); 436 | notesForm.data( 'task', task ); 437 | 438 | notesMde = new SimpleMDE({ 439 | element: notesForm.find( 'textarea' ).val( task.notes )[0], 440 | status: false, 441 | spellChecker: false 442 | }); 443 | notesMde.codemirror.focus(); 444 | notesMde.codemirror.setCursor(notesMde.codemirror.lineCount(), 0); 445 | 446 | notesForm.on( 'click', '.btn-save', closeEditorSave ); 447 | notesForm.on( 'click', '.btn-cancel', closeEditor ); 448 | 449 | list.isClick = false; 450 | } 451 | 452 | function closeEditor() { 453 | notesForm.find( '.task-title' ).text(''); 454 | 455 | notesForm.off( 'click', '.btn-save', closeEditorSave ); 456 | notesForm.off( 'click', '.btn-cancel', closeEditor ); 457 | 458 | notesMde.toTextArea(); 459 | notesMde = null; 460 | 461 | notesForm.hide(); 462 | } 463 | 464 | function editorMouseDown( ev ) { 465 | list.isClick = true 466 | list.downPos = { 467 | sum: ev.offsetX + ev.offsetY, 468 | x: ev.offsetX, 469 | y: ev.offsetY 470 | }; 471 | } 472 | 473 | function editorMouseMove( ev ) { 474 | if ( ! list.isClick ) { 475 | return; 476 | } 477 | 478 | var moved = Math.abs(ev.offsetX + ev.offsetY - list.downPos.sum) 479 | 480 | if (moved > 5) { 481 | list.isClick = false; 482 | } 483 | } 484 | 485 | function closeEditorSave() { 486 | var task = notesForm.data( 'task' ); 487 | task.notes = notesMde.value(); 488 | 489 | updateTask( task, closeEditor ); 490 | } 491 | 492 | function setColorFlag() { 493 | var flag = jQuery( this ); 494 | var row = flag.closest( 'tr' ); 495 | var task = row.data( 'task' ); 496 | 497 | task.flag = flag.data( 'flag' ); 498 | updateTask( task ); 499 | } 500 | 501 | function updateTask( task, onDone ) { 502 | var ajaxTask = _.clone( task ); 503 | delete ajaxTask.$el; 504 | delete ajaxTask.notes_html; 505 | delete ajaxTask.client_name; 506 | delete ajaxTask.avatar; 507 | 508 | jQuery.post( 509 | window.ajaxurl, 510 | { 511 | action: 'wpcable_update_task', 512 | _wpnonce: window.wpcable.update_task_nonce, 513 | task: ajaxTask 514 | }, 515 | function done( res ) { 516 | task.$el.trigger( 'task:refresh' ); 517 | 518 | updateFilters(); 519 | initFilters(); 520 | 521 | if ( 'function' === typeof onDone ) { 522 | onDone(); 523 | } 524 | } 525 | ); 526 | } 527 | 528 | notesForm.hide(); 529 | 530 | initFilters(); 531 | updateFilters(); 532 | refreshList(); 533 | 534 | notesMde = new SimpleMDE({ 535 | element: notesForm.find( 'textarea' )[0], 536 | status: false, 537 | spellChecker: false 538 | }); 539 | 540 | jQuery( window ).on( 'hashchange', updateFilters ); 541 | jQuery( document ).on( 'codeable-reload-tasks', reloadData ); 542 | 543 | filterCb.on( 'click', function() { updateFilters(); initFilters(); } ); 544 | flagCb.on( 'click', function() { updateFilters(); initFilters(); } ); 545 | 546 | filterTxt.on( 'change keyup search', function() { 547 | if ( filterTxtVal !== filterTxt.val() ) { 548 | updateFilters(); 549 | initFilters(); 550 | } 551 | } ); 552 | 553 | list.on( 'click', '.color-flag [data-flag]', setColorFlag ); 554 | list.on( 'click', '.notes-body', startEditor ) 555 | .on( 'mousedown', '.notes-body', editorMouseDown) 556 | .on( 'mousemove', '.notes-body', editorMouseMove); 557 | }); 558 | -------------------------------------------------------------------------------- /assets/js/highcharts-3d.js: -------------------------------------------------------------------------------- 1 | /* 2 | Highcharts JS v5.0.7 (2017-01-17) 3 | 4 | 3D features for Highcharts JS 5 | 6 | @license: www.highcharts.com/license 7 | */ 8 | (function(F){"object"===typeof module&&module.exports?module.exports=F:F(Highcharts)})(function(F){(function(a){var r=a.deg2rad,k=a.pick;a.perspective=function(n,m,u){var p=m.options.chart.options3d,f=u?m.inverted:!1,g=m.plotWidth/2,q=m.plotHeight/2,d=p.depth/2,e=k(p.depth,1)*k(p.viewDistance,0),c=m.scale3d||1,b=r*p.beta*(f?-1:1),p=r*p.alpha*(f?-1:1),h=Math.cos(p),w=Math.cos(-b),y=Math.sin(p),z=Math.sin(-b);u||(g+=m.plotLeft,q+=m.plotTop);return a.map(n,function(b){var a,p;p=(f?b.y:b.x)-g;var m=(f? 9 | b.x:b.y)-q,k=(b.z||0)-d;a=w*p-z*k;b=-y*z*p+h*m-w*y*k;p=h*z*p+y*m+h*w*k;m=0a&&h-a>Math.PI/2+.0001?(D=D.concat(n(b,c,l,x,a,a+Math.PI/2,d,e)),D=D.concat(n(b,c,l,x,a+Math.PI/2,h,d,e))):hMath.PI/2+.0001?(D=D.concat(n(b,c,l,x,a,a-Math.PI/2,d,e)),D=D.concat(n(b,c,l,x,a-Math.PI/2,h,d,e))):["C",b+l*Math.cos(a)-l*t*f*Math.sin(a)+d,c+x*Math.sin(a)+x*t*f*Math.cos(a)+e,b+l*Math.cos(h)+l*t*f*Math.sin(h)+d,c+x*Math.sin(h)- 11 | x*t*f*Math.cos(h)+e,b+l*Math.cos(h)+d,c+x*Math.sin(h)+e]}var m=Math.cos,u=Math.PI,p=Math.sin,f=a.animObject,g=a.charts,q=a.color,d=a.defined,e=a.deg2rad,c=a.each,b=a.extend,h=a.inArray,w=a.map,y=a.merge,z=a.perspective,G=a.pick,B=a.SVGElement,H=a.SVGRenderer,C=a.wrap,t=4*(Math.sqrt(2)-1)/3/(u/2);H.prototype.toLinePath=function(b,a){var h=[];c(b,function(b){h.push("L",b.x,b.y)});b.length&&(h[0]="M",a&&h.push("Z"));return h};H.prototype.cuboid=function(b){var c=this.g(),h=c.destroy;b=this.cuboidPath(b); 12 | c.attr({"stroke-linejoin":"round"});c.front=this.path(b[0]).attr({"class":"highcharts-3d-front",zIndex:b[3]}).add(c);c.top=this.path(b[1]).attr({"class":"highcharts-3d-top",zIndex:b[4]}).add(c);c.side=this.path(b[2]).attr({"class":"highcharts-3d-side",zIndex:b[5]}).add(c);c.fillSetter=function(b){this.front.attr({fill:b});this.top.attr({fill:q(b).brighten(.1).get()});this.side.attr({fill:q(b).brighten(-.1).get()});this.color=b;return this};c.opacitySetter=function(b){this.front.attr({opacity:b}); 13 | this.top.attr({opacity:b});this.side.attr({opacity:b});return this};c.attr=function(b){if(b.shapeArgs||d(b.x))b=this.renderer.cuboidPath(b.shapeArgs||b),this.front.attr({d:b[0],zIndex:b[3]}),this.top.attr({d:b[1],zIndex:b[4]}),this.side.attr({d:b[2],zIndex:b[5]});else return a.SVGElement.prototype.attr.call(this,b);return this};c.animate=function(b,c,a){d(b.x)&&d(b.y)?(b=this.renderer.cuboidPath(b),this.front.attr({zIndex:b[3]}).animate({d:b[0]},c,a),this.top.attr({zIndex:b[4]}).animate({d:b[1]}, 14 | c,a),this.side.attr({zIndex:b[5]}).animate({d:b[2]},c,a),this.attr({zIndex:-b[6]})):b.opacity?(this.front.animate(b,c,a),this.top.animate(b,c,a),this.side.animate(b,c,a)):B.prototype.animate.call(this,b,c,a);return this};c.destroy=function(){this.front.destroy();this.top.destroy();this.side.destroy();return h.call(this)};c.attr({zIndex:-b[6]});return c};H.prototype.cuboidPath=function(b){function c(b){return q[b]}var a=b.x,h=b.y,d=b.z,e=b.height,D=b.width,f=b.depth,q=[{x:a,y:h,z:d},{x:a+D,y:h,z:d}, 15 | {x:a+D,y:h+e,z:d},{x:a,y:h+e,z:d},{x:a,y:h+e,z:d+f},{x:a+D,y:h+e,z:d+f},{x:a+D,y:h,z:d+f},{x:a,y:h,z:d+f}],q=z(q,g[this.chartIndex],b.insidePlotArea),d=function(b,a){var h=[];b=w(b,c);a=w(a,c);0>r(b)?h=b:0>r(a)&&(h=a);return h};b=d([3,2,1,0],[7,6,5,4]);a=[4,5,2,3];h=d([1,6,7,0],a);d=d([1,2,5,6],[0,7,4,3]);return[this.toLinePath(b,!0),this.toLinePath(h,!0),this.toLinePath(d,!0),k(b),k(h),k(d),9E9*k(w(a,c))]};a.SVGRenderer.prototype.arc3d=function(a){function d(b){var a=!1,c={},d;for(d in b)-1!==h(d, 16 | p)&&(c[d]=b[d],delete b[d],a=!0);return a?c:!1}var l=this.g(),x=l.renderer,p="x y r innerR start end".split(" ");a=y(a);a.alpha*=e;a.beta*=e;l.top=x.path();l.side1=x.path();l.side2=x.path();l.inn=x.path();l.out=x.path();l.onAdd=function(){var b=l.parentGroup,a=l.attr("class");l.top.add(l);c(["out","inn","side1","side2"],function(c){l[c].addClass(a+" highcharts-3d-side").add(b)})};l.setPaths=function(b){var a=l.renderer.arc3dPath(b),c=100*a.zTop;l.attribs=b;l.top.attr({d:a.top,zIndex:a.zTop});l.inn.attr({d:a.inn, 17 | zIndex:a.zInn});l.out.attr({d:a.out,zIndex:a.zOut});l.side1.attr({d:a.side1,zIndex:a.zSide1});l.side2.attr({d:a.side2,zIndex:a.zSide2});l.zIndex=c;l.attr({zIndex:c});b.center&&(l.top.setRadialReference(b.center),delete b.center)};l.setPaths(a);l.fillSetter=function(b){var a=q(b).brighten(-.1).get();this.fill=b;this.side1.attr({fill:a});this.side2.attr({fill:a});this.inn.attr({fill:a});this.out.attr({fill:a});this.top.attr({fill:b});return this};c(["opacity","translateX","translateY","visibility"], 18 | function(b){l[b+"Setter"]=function(b,a){l[a]=b;c(["out","inn","side1","side2","top"],function(c){l[c].attr(a,b)})}});C(l,"attr",function(a,c){var h;"object"===typeof c&&(h=d(c))&&(b(l.attribs,h),l.setPaths(l.attribs));return a.apply(this,[].slice.call(arguments,1))});C(l,"animate",function(b,a,c,h){var l,e=this.attribs,q;delete a.center;delete a.z;delete a.depth;delete a.alpha;delete a.beta;q=f(G(c,this.renderer.globalAnimation));q.duration&&(a=y(a),l=d(a),a.dummy=1,l&&(q.step=function(b,a){function c(b){return e[b]+ 19 | (G(l[b],e[b])-e[b])*a.pos}"dummy"===a.prop&&a.elem.setPaths(y(e,{x:c("x"),y:c("y"),r:c("r"),innerR:c("innerR"),start:c("start"),end:c("end")}))}),c=q);return b.call(this,a,c,h)});l.destroy=function(){this.top.destroy();this.out.destroy();this.inn.destroy();this.side1.destroy();this.side2.destroy();B.prototype.destroy.call(this)};l.hide=function(){this.top.hide();this.out.hide();this.inn.hide();this.side1.hide();this.side2.hide()};l.show=function(){this.top.show();this.out.show();this.inn.show();this.side1.show(); 20 | this.side2.show()};return l};H.prototype.arc3dPath=function(b){function a(b){b%=2*Math.PI;b>Math.PI&&(b=2*Math.PI-b);return b}var c=b.x,h=b.y,d=b.start,e=b.end-.00001,f=b.r,q=b.innerR,w=b.depth,g=b.alpha,k=b.beta,y=Math.cos(d),r=Math.sin(d);b=Math.cos(e);var z=Math.sin(e),v=f*Math.cos(k),f=f*Math.cos(g),t=q*Math.cos(k),C=q*Math.cos(g),q=w*Math.sin(k),A=w*Math.sin(g),w=["M",c+v*y,h+f*r],w=w.concat(n(c,h,v,f,d,e,0,0)),w=w.concat(["L",c+t*b,h+C*z]),w=w.concat(n(c,h,t,C,e,d,0,0)),w=w.concat(["Z"]),G= 21 | 0-G?d:e>-G?-G:d,E=eB&&du-k&&dq&&(t=Math.min(t,1-Math.abs((e+f)/(q+f))%1));cp&&(t=0>p?Math.min(t,(b+g)/(-p+b+g)):Math.min(t,1-(b+g)/(p+g)%1));h=g.min&&e<=g.max:!1,m.push({x:d.plotX,y:d.plotY,z:d.plotZ});f=r(m,f,!0);for(c=0;ctables = array( 21 | 'transcactions' => $wpdb->prefix . 'codeable_transcactions', 22 | 'clients' => $wpdb->prefix . 'codeable_clients', 23 | 'amounts' => $wpdb->prefix . 'codeable_amounts', 24 | ); 25 | 26 | } 27 | 28 | public function get_dates_totals( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ) { 29 | 30 | $first_date = date( 'Y-m-d H:i:s', strtotime( $from_year . '-' . $from_month . '-' . $from_day ) ); 31 | $last_date = date( 'Y-m-d H:i:s', strtotime( $to_year . '-' . $to_month . '-' . $to_day . ' 23:59:59' ) ); 32 | 33 | $query = ' 34 | SELECT 35 | SUM(fee_amount) as fee_amount, 36 | SUM(credit_fee_amount) as contractor_fee, 37 | SUM(credit_revenue_amount) as revenue, 38 | SUM(debit_user_amount) as total_cost, 39 | count(1) as tasks 40 | FROM 41 | ' . $this->tables['transcactions'] . ' LEFT JOIN ' . $this->tables['amounts'] . ' 42 | ON 43 | ' . $this->tables['transcactions'] . '.task_id = ' . $this->tables['amounts'] . ".task_id 44 | WHERE 45 | `description` = 'task_completion' 46 | AND (dateadded BETWEEN '" . $first_date . "' AND '" . $last_date . "') 47 | "; 48 | 49 | // check cache 50 | $cache_key = 'date_totals_' . $first_date . '_' . $last_date; 51 | $result = $this->check_cache( $cache_key, $query ); 52 | 53 | $single_result = array_shift( $result ); 54 | 55 | return $single_result; 56 | 57 | } 58 | 59 | public function get_days( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ) { 60 | 61 | $first_date = date( 'Y-m-d H:i:s', strtotime( $from_year . '-' . $from_month . '-' . $from_day ) ); 62 | $last_date = date( 'Y-m-d H:i:s', strtotime( $to_year . '-' . $to_month . '-' . $to_day . ' 23:59:59' ) ); 63 | 64 | $query = ' 65 | SELECT 66 | fee_amount as fee_amount, 67 | credit_fee_amount as contractor_fee, 68 | credit_revenue_amount as revenue, 69 | debit_user_amount as total_cost, 70 | dateadded 71 | FROM 72 | ' . $this->tables['transcactions'] . ' LEFT JOIN ' . $this->tables['amounts'] . ' 73 | ON 74 | ' . $this->tables['transcactions'] . '.task_id = ' . $this->tables['amounts'] . ".task_id 75 | WHERE 76 | `description` = 'task_completion' 77 | AND (dateadded BETWEEN '" . $first_date . "' AND '" . $last_date . "') 78 | "; 79 | 80 | // check cache 81 | $cache_key = 'days_' . $first_date . '_' . $last_date; 82 | $result = $this->check_cache( $cache_key, $query ); 83 | 84 | $days_totals = array(); 85 | foreach ( $result as $single_payment ) { 86 | 87 | $datekey = date( 'Ymd', strtotime( $single_payment['dateadded'] ) ); 88 | 89 | if ( isset( $days_totals[ $datekey ] ) ) { 90 | 91 | $days_totals[ $datekey ]['fee_amount'] += $single_payment['fee_amount']; 92 | $days_totals[ $datekey ]['contractor_fee'] += $single_payment['contractor_fee']; 93 | $days_totals[ $datekey ]['revenue'] += $single_payment['revenue']; 94 | $days_totals[ $datekey ]['total_cost'] += $single_payment['total_cost']; 95 | $days_totals[ $datekey ]['tasks'] = $days_totals[ $datekey ]['tasks'] + 1; 96 | 97 | } else { 98 | 99 | $days_totals[ $datekey ] = $single_payment; 100 | $days_totals[ $datekey ]['tasks'] = 1; 101 | 102 | } 103 | } 104 | 105 | return $days_totals; 106 | 107 | } 108 | 109 | public function get_dates_average( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ) { 110 | 111 | $first_date = date( 'Y-m-d H:i:s', strtotime( $from_year . '-' . $from_month . '-' . $from_day ) ); 112 | $last_date = date( 'Y-m-d H:i:s', strtotime( $to_year . '-' . $to_month . '-' . $to_day . ' 23:59:59' ) ); 113 | 114 | $query = ' 115 | SELECT 116 | AVG(fee_amount) as fee_amount, 117 | AVG(credit_fee_amount) as contractor_fee, 118 | AVG(credit_revenue_amount) as revenue, 119 | AVG(debit_user_amount) as total_cost 120 | FROM 121 | ' . $this->tables['transcactions'] . ' LEFT JOIN ' . $this->tables['amounts'] . ' 122 | ON 123 | ' . $this->tables['transcactions'] . '.task_id = ' . $this->tables['amounts'] . ".task_id 124 | WHERE 125 | `description` = 'task_completion' 126 | AND (dateadded BETWEEN '" . $first_date . "' AND '" . $last_date . "') 127 | "; 128 | 129 | // check cache 130 | $cache_key = 'dates_average' . $first_date . '_' . $last_date; 131 | $result = $this->check_cache( $cache_key, $query ); 132 | 133 | $single_result = array_shift( $result ); 134 | 135 | return $single_result; 136 | 137 | } 138 | 139 | public function get_months_average( $from_month, $from_year, $to_month, $to_year ) { 140 | 141 | $get_month_range_totals = $this->get_month_range_totals( $from_month, $from_year, $to_month, $to_year ); 142 | 143 | $fee_amount = $contractor_fee = $revenue = $total_cost = $tasks = array(); 144 | 145 | foreach ( $get_month_range_totals as $month ) { 146 | 147 | $fee_amount[] = $month['fee_amount']; 148 | $contractor_fee[] = $month['contractor_fee']; 149 | $revenue[] = $month['revenue']; 150 | $total_cost[] = $month['total_cost']; 151 | $tasks[] = $month['tasks']; 152 | 153 | } 154 | 155 | $averages = array( 156 | 'fee_amount' => round( ( array_sum( $fee_amount ) / count( $fee_amount ) ), 2 ), 157 | 'contractor_fee' => round( ( array_sum( $contractor_fee ) / count( $contractor_fee ) ), 2 ), 158 | 'revenue' => round( ( array_sum( $revenue ) / count( $revenue ) ), 2 ), 159 | 'total_cost' => round( ( array_sum( $total_cost ) / count( $total_cost ) ), 2 ), 160 | ); 161 | 162 | return $averages; 163 | 164 | } 165 | 166 | 167 | public function get_month_totals( $month, $year ) { 168 | 169 | $to_day = date( 't', strtotime( $year . '-' . $month . '-01 23:59:59' ) ); 170 | 171 | return $this->get_dates_totals( '01', $month, $year, $to_day, $month, $year ); 172 | 173 | } 174 | 175 | public function get_month_range_totals( $from_month = '', $from_year = '', $to_month = '', $to_year = '' ) { 176 | 177 | $totals = array(); 178 | 179 | $firstdate = ''; 180 | $lastdate = ''; 181 | 182 | // get first and last task if no date is set 183 | if ( $from_month == '' && $from_year == '' ) { 184 | $get_first_task = $this->get_first_task(); 185 | $firstdate = $get_first_task['dateadded']; 186 | } else { 187 | $firstdate = $from_year . '-' . $from_month . '-01'; 188 | } 189 | if ( $to_month == '' && $to_year == '' ) { 190 | $get_last_task = $this->get_last_task(); 191 | $lastdate = $get_last_task['dateadded']; 192 | } else { 193 | $lastdate = $to_year . '-' . $to_month . '-' . date( 't', strtotime( $to_year . '-' . $to_month . '-01 23:59:59' ) ); 194 | } 195 | 196 | $begin = new DateTime( $firstdate ); 197 | $end = new DateTime( $lastdate ); 198 | 199 | $interval = DateInterval::createFromDateString( '1 month' ); 200 | $period = new DatePeriod( $begin, $interval, $end ); 201 | 202 | foreach ( $period as $dt ) { 203 | 204 | $totals[ $dt->format( 'Ym' ) ] = $this->get_month_totals( $dt->format( 'm' ), $dt->format( 'Y' ) ); 205 | 206 | } 207 | 208 | return $totals; 209 | 210 | } 211 | 212 | 213 | public function get_first_task() { 214 | 215 | $query = ' 216 | SELECT 217 | * 218 | FROM 219 | ' . $this->tables['transcactions'] . ' LEFT JOIN ' . $this->tables['amounts'] . ' 220 | ON 221 | ' . $this->tables['transcactions'] . '.task_id = ' . $this->tables['amounts'] . ".task_id 222 | WHERE 223 | `description` = 'task_completion' 224 | ORDER BY " . $this->tables['transcactions'] . '.id ASC 225 | LIMIT 0,1 226 | '; 227 | 228 | // check cache 229 | $cache_key = 'first_task'; 230 | $result = $this->check_cache( $cache_key, $query ); 231 | 232 | return array_shift( $result ); 233 | } 234 | 235 | public function get_last_task() { 236 | 237 | $query = ' 238 | SELECT 239 | * 240 | FROM 241 | ' . $this->tables['transcactions'] . ' LEFT JOIN ' . $this->tables['amounts'] . ' 242 | ON 243 | ' . $this->tables['transcactions'] . '.task_id = ' . $this->tables['amounts'] . ".task_id 244 | WHERE 245 | `description` = 'task_completion' 246 | ORDER BY " . $this->tables['transcactions'] . '.id DESC 247 | LIMIT 0,1 248 | '; 249 | 250 | // check cache 251 | $cache_key = 'last_task'; 252 | $result = $this->check_cache( $cache_key, $query ); 253 | 254 | return array_shift( $result ); 255 | } 256 | 257 | public function get_year_totals( $year ) { 258 | 259 | return $this->get_dates_totals( '01', '01', $year, '31', '12', $year ); 260 | 261 | } 262 | 263 | 264 | public function get_amounts_range( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ) { 265 | 266 | $first_date = date( 'Y-m-d H:i:s', strtotime( $from_year . '-' . $from_month . '-' . $from_day ) ); 267 | $last_date = date( 'Y-m-d H:i:s', strtotime( $to_year . '-' . $to_month . '-' . $to_day . ' 23:59:59' ) ); 268 | 269 | $query = ' 270 | SELECT 271 | credit_revenue_amount 272 | FROM 273 | ' . $this->tables['transcactions'] . ' LEFT JOIN ' . $this->tables['amounts'] . ' 274 | ON 275 | ' . $this->tables['transcactions'] . '.task_id = ' . $this->tables['amounts'] . ".task_id 276 | WHERE 277 | `description` = 'task_completion' 278 | AND (dateadded BETWEEN '" . $first_date . "' AND '" . $last_date . "') 279 | "; 280 | 281 | // check cache 282 | $cache_key = 'amounts_range_' . $first_date . '_' . $last_date; 283 | $result = $this->check_cache( $cache_key, $query ); 284 | 285 | $variance = array( 286 | '0-100' => 0, 287 | '100-300' => 0, 288 | '300-500' => 0, 289 | '500-1000' => 0, 290 | '1000-3000' => 0, 291 | '3000-5000' => 0, 292 | '5000-10000' => 0, 293 | '10000-20000' => 0, 294 | ); 295 | $milestones = array( 0, 100, 300, 500, 1000, 3000, 5000, 10000, 20000 ); 296 | 297 | foreach ( $result as $amount ) { 298 | $revenue = $amount['credit_revenue_amount']; 299 | 300 | for ( $i = 0; $i < count( $milestones ); $i ++ ) { 301 | if ( $revenue > $milestones[ $i ] && isset( $milestones[ $i + 1 ] ) && $revenue <= $milestones[ $i + 1 ] ) { 302 | $variance[ $milestones[ $i ] . '-' . $milestones[ $i + 1 ] ] ++; 303 | } 304 | } 305 | } 306 | 307 | return $variance; 308 | 309 | } 310 | 311 | 312 | public function get_tasks_per_month( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ) { 313 | 314 | $first_date = date( 'Y-m-d H:i:s', strtotime( $from_year . '-' . $from_month . '-' . $from_day ) ); 315 | $last_date = date( 'Y-m-d H:i:s', strtotime( $to_year . '-' . $to_month . '-' . $to_day . ' 23:59:59' ) ); 316 | 317 | $query = " 318 | SELECT 319 | DATE_FORMAT(dateadded,'%Y-%m') as dateadded, count(1) as tasks_per_month 320 | FROM 321 | " . $this->tables['transcactions'] . " 322 | WHERE 323 | `description` = 'task_completion' 324 | AND (dateadded BETWEEN '" . $first_date . "' AND '" . $last_date . "') 325 | GROUP BY 326 | YEAR(dateadded), MONTH(dateadded) DESC 327 | ORDER BY 328 | `dateadded` ASC 329 | "; 330 | 331 | // check cache 332 | $cache_key = 'tasks_per_month_' . $first_date . '_' . $last_date; 333 | $result = $this->check_cache( $cache_key, $query ); 334 | 335 | return $result; 336 | 337 | } 338 | 339 | public function get_tasks_type( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ) { 340 | 341 | $first_date = date( 'Y-m-d H:i:s', strtotime( $from_year . '-' . $from_month . '-' . $from_day ) ); 342 | $last_date = date( 'Y-m-d H:i:s', strtotime( $to_year . '-' . $to_month . '-' . $to_day . ' 23:59:59' ) ); 343 | 344 | $query = ' 345 | SELECT 346 | task_type, 347 | COUNT(id) as count, 348 | SUM(debit_user_amount) as user_amount, 349 | SUM(credit_revenue_amount) as revenue, 350 | SUM(credit_fee_amount) as fee 351 | FROM 352 | ' . $this->tables['transcactions'] . ' LEFT JOIN ' . $this->tables['amounts'] . ' 353 | ON 354 | ' . $this->tables['transcactions'] . '.task_id = ' . $this->tables['amounts'] . ".task_id 355 | WHERE 356 | `description` = 'task_completion' 357 | AND (dateadded BETWEEN '" . $first_date . "' AND '" . $last_date . "') 358 | GROUP BY task_type 359 | "; 360 | 361 | // check cache 362 | $cache_key = 'tasks_type_' . $first_date . '_' . $last_date; 363 | $result = $this->check_cache( $cache_key, $query ); 364 | 365 | $out = array(); 366 | 367 | foreach ( $result as $res ) { 368 | 369 | if ( ! $res['revenue'] ) { 370 | continue; 371 | } 372 | 373 | $out[ $res['task_type'] ] = $res; 374 | 375 | } 376 | 377 | return $out; 378 | 379 | } 380 | 381 | public function get_preferred_count( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ) { 382 | 383 | $first_date = date( 'Y-m-d H:i:s', strtotime( $from_year . '-' . $from_month . '-' . $from_day ) ); 384 | $last_date = date( 'Y-m-d H:i:s', strtotime( $to_year . '-' . $to_month . '-' . $to_day . ' 23:59:59' ) ); 385 | 386 | $query = ' 387 | SELECT 388 | preferred, 389 | COUNT(id) as count, 390 | SUM(debit_user_amount) as user_amount, 391 | SUM(credit_revenue_amount) as revenue, 392 | SUM(credit_fee_amount) as fee 393 | FROM 394 | ' . $this->tables['transcactions'] . ' LEFT JOIN ' . $this->tables['amounts'] . ' 395 | ON 396 | ' . $this->tables['transcactions'] . '.task_id = ' . $this->tables['amounts'] . ".task_id 397 | WHERE 398 | `description` = 'task_completion' 399 | AND (preferred = 1 OR preferred = 0) 400 | AND (dateadded BETWEEN '" . $first_date . "' AND '" . $last_date . "') 401 | GROUP BY preferred 402 | "; 403 | 404 | // check cache 405 | $cache_key = 'preferred_count_' . $first_date . '_' . $last_date; 406 | $result = $this->check_cache( $cache_key, $query ); 407 | 408 | $out = array( 409 | 'preferred' => 0, 410 | 'nonpreferred' => 0, 411 | ); 412 | 413 | foreach ( $result as $res ) { 414 | if ( $res['preferred'] == 1 ) { 415 | $out['preferred'] = $res; 416 | } 417 | if ( $res['preferred'] == 0 ) { 418 | $out['nonpreferred'] = $res; 419 | } 420 | } 421 | 422 | return $out; 423 | 424 | } 425 | 426 | 427 | // returns an array with all stats 428 | public function get_all_stats( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year, $chart_display_method = 'months' ) { 429 | 430 | $stats = array(); 431 | 432 | $averages = $this->get_months_average( $from_month, $from_year, $to_month, $to_year ); 433 | $preferred_count = $this->get_preferred_count( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ); 434 | $get_amounts_range = $this->get_amounts_range( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ); 435 | 436 | $chart_amounts_range = array(); 437 | $get_available_ranges = array(); 438 | foreach ( $get_amounts_range as $range => $num_of_tasks ) { 439 | $chart_amounts_range[] = '["' . $range . '", ' . $num_of_tasks . ']'; 440 | $get_available_ranges[] = '"' . $range . '"'; 441 | } 442 | 443 | $get_tasks_type = $this->get_tasks_type( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ); 444 | 445 | $type_categories = array(); 446 | $type_contractor_fee = array(); 447 | $type_revenue = array(); 448 | $type_tasks_count = array(); 449 | 450 | foreach ( $get_tasks_type as $type => $type_data ) { 451 | 452 | $type_categories[ $type ] = "'" . $type . "'"; 453 | $type_contractor_fee[ $type ] = floatval( $type_data['fee'] ); 454 | $type_revenue[ $type ] = floatval( $type_data['revenue'] ); 455 | $type_tasks_count[ $type ] = intval( $type_data['count'] ); 456 | } 457 | 458 | $type_tasks_count_json = json_encode( $type_tasks_count ); 459 | 460 | if ( $chart_display_method == 'months' ) { 461 | 462 | $month_totals = $this->get_month_range_totals( $from_month, $from_year, $to_month, $to_year ); 463 | 464 | $max_month_totals = max( $month_totals ); 465 | $max_month_totals_key = array_keys( $month_totals, max( $month_totals ) ); 466 | 467 | $all_month_totals = array(); 468 | $all_month_totals['revenue'] = $all_month_totals['total_cost'] = ''; 469 | foreach ( $month_totals as $mt ) { 470 | $all_month_totals['revenue'] = floatval( $all_month_totals['revenue'] ) + floatval( $mt['revenue'] ); 471 | $all_month_totals['total_cost'] = floatval( $all_month_totals['total_cost'] ) + floatval( $mt['total_cost'] ); 472 | } 473 | 474 | $chart_categories = array(); 475 | $chart_dates = array(); 476 | $chart_contractor_fee = array(); 477 | $chart_revenue = array(); 478 | $chart_revenue_avg = array(); 479 | $chart_total_cost = array(); 480 | $chart_tasks_count = array(); 481 | $chart_tasks_count_avg = array(); 482 | 483 | foreach ( $month_totals as $yearmonth => $amounts ) { 484 | 485 | $chart_categories[ $yearmonth ] = "'" . wordwrap( $yearmonth, 4, '-', true ) . "'"; 486 | $chart_dates[] = wordwrap( $yearmonth, 4, '-', true ); 487 | $chart_contractor_fee[ $yearmonth ] = floatval( $amounts['fee_amount'] ); 488 | $chart_revenue[ $yearmonth ] = floatval( $amounts['revenue'] ); 489 | $chart_total_cost[ $yearmonth ] = floatval( $amounts['total_cost'] ); 490 | $chart_tasks_count[ $yearmonth ] = intval( $amounts['tasks'] ); 491 | 492 | } 493 | 494 | $chart_tasks_count_json = json_encode( $chart_tasks_count ); 495 | $chart_revenue_json = json_encode( $chart_revenue ); 496 | 497 | } else { 498 | 499 | $days_totals = $this->get_days( $from_day, $from_month, $from_year, $to_day, $to_month, $to_year ); 500 | 501 | $max_month_totals = max( $days_totals ); 502 | $max_month_totals_key = array_keys( $days_totals, max( $days_totals ) ); 503 | $max_month_totals_key[0] = wordwrap( $max_month_totals_key[0], 6, '-', true ); 504 | 505 | $all_month_totals = array(); 506 | foreach ( $days_totals as $mt ) { 507 | if ( ! isset( $all_month_totals['revenue'] ) ) { 508 | $all_month_totals['revenue'] = 0; } 509 | if ( ! isset( $all_month_totals['total_cost'] ) ) { 510 | $all_month_totals['total_cost'] = 0; } 511 | 512 | $all_month_totals['revenue'] = $all_month_totals['revenue'] + $mt['revenue']; 513 | $all_month_totals['total_cost'] = $all_month_totals['total_cost'] + $mt['total_cost']; 514 | } 515 | 516 | $chart_categories = array(); 517 | $chart_dates = array(); 518 | $chart_contractor_fee = array(); 519 | $chart_revenue = array(); 520 | $chart_revenue_avg = array(); 521 | $chart_total_cost = array(); 522 | $chart_tasks_count = array(); 523 | $chart_tasks_count_avg = array(); 524 | 525 | foreach ( $days_totals as $yearmonthday => $amounts ) { 526 | 527 | $date_array = array(); 528 | $date_array = date_parse_from_format( 'Ymd', $yearmonthday ); 529 | 530 | $chart_categories[ $yearmonthday ] = "'" . $date_array['year'] . '-' . sprintf( '%02d', $date_array['month'] ) . '-' . sprintf( '%02d', $date_array['day'] ) . "'"; 531 | $chart_dates[] = $date_array['year'] . '-' . sprintf( '%02d', $date_array['month'] ) . '-' . sprintf( '%02d', $date_array['day'] ); 532 | $chart_contractor_fee[ $yearmonthday ] = floatval( $amounts['fee_amount'] ); 533 | $chart_revenue[ $yearmonthday ] = floatval( $amounts['revenue'] ); 534 | $chart_total_cost[ $yearmonthday ] = floatval( $amounts['total_cost'] ); 535 | $chart_tasks_count[ $yearmonthday ] = intval( $amounts['tasks'] ); 536 | } 537 | 538 | $chart_tasks_count_json = json_encode( $chart_tasks_count ); 539 | $chart_revenue_json = json_encode( $chart_revenue ); 540 | 541 | } 542 | 543 | $chart_dates_json = json_encode( $chart_dates ); 544 | 545 | $fromDT = new DateTime( $from_year . '-' . $from_month . '-' . $from_day ); 546 | $toDT = new DateTime( $to_year . '-' . $to_month . '-' . $to_day ); 547 | 548 | $datediff = date_diff( $fromDT, $toDT ); 549 | 550 | if ( $chart_display_method == 'months' ) { 551 | $datediffcount = $datediff->format( '%m' ) + ( $datediff->format( '%y' ) * 12 ) + 1; 552 | } 553 | if ( $chart_display_method == 'days' ) { 554 | $datediffcount = $datediff->format( '%a' ); 555 | } 556 | 557 | $chart_revenue_avg = array_fill( 0, count( $chart_revenue ), round( array_sum( $chart_revenue ) / $datediffcount, 2 ) ); 558 | $chart_tasks_count_avg = array_fill( 0, count( $chart_tasks_count ), round( array_sum( $chart_tasks_count ) / $datediffcount, 2 ) ); 559 | 560 | $stats['averages'] = $averages; 561 | $stats['preferred_count'] = $preferred_count; 562 | $stats['chart_amounts_range'] = $chart_amounts_range; 563 | $stats['get_available_ranges'] = $get_available_ranges; 564 | $stats['type_categories'] = $type_categories; 565 | $stats['type_contractor_fee'] = $type_contractor_fee; 566 | $stats['type_revenue'] = $type_revenue; 567 | $stats['type_tasks_count'] = $type_tasks_count; 568 | $stats['type_tasks_count_json'] = $type_tasks_count_json; 569 | $stats['max_month_totals'] = $max_month_totals; 570 | $stats['max_month_totals_key'] = $max_month_totals_key; 571 | $stats['all_month_totals'] = $all_month_totals; 572 | $stats['chart_categories'] = $chart_categories; 573 | $stats['chart_dates'] = $chart_dates; 574 | $stats['chart_dates_json'] = $chart_dates_json; 575 | $stats['chart_contractor_fee'] = $chart_contractor_fee; 576 | $stats['chart_revenue'] = $chart_revenue; 577 | $stats['chart_revenue_avg'] = $chart_revenue_avg; 578 | $stats['chart_total_cost'] = $chart_total_cost; 579 | $stats['chart_tasks_count'] = $chart_tasks_count; 580 | $stats['chart_tasks_count_avg'] = $chart_tasks_count_avg; 581 | $stats['chart_tasks_count_json'] = $chart_tasks_count_json; 582 | $stats['chart_revenue_json'] = $chart_revenue_json; 583 | 584 | return $stats; 585 | } 586 | 587 | /** 588 | * Checks and sets cached data 589 | * 590 | * @since 0.0.6 591 | * @author Justin Frydman 592 | * 593 | * @param bool $key The unique cache key 594 | * @param bool $query The query to check 595 | * 596 | * @return mixed The raw or cached data 597 | * @throws Exception 598 | */ 599 | private function check_cache( $key = false, $query = false ) { 600 | 601 | $cache = new wpcable_cache( $key, $query ); 602 | return $cache->check(); 603 | 604 | } 605 | 606 | } 607 | --------------------------------------------------------------------------------