diff --git a/app/controllers/GoogleTableController.php b/app/controllers/GoogleTableController.php index 592aeb8ea9..7ff6392524 100644 --- a/app/controllers/GoogleTableController.php +++ b/app/controllers/GoogleTableController.php @@ -1,5 +1,6 @@ addColumn('ID_Delete', 'string'); $chart->addColumn('Name_URL', 'string'); $chart->addColumn('Name', 'string'); - $chart->addColumn('Matches','string'); - $chart->addColumn('Minimum amount','number'); - $chart->addColumn('Maximum amount','number'); + $chart->addColumn('Matches', 'string'); + $chart->addColumn('Minimum amount', 'number'); + $chart->addColumn('Maximum amount', 'number'); /** @var \FireflyIII\Database\Recurring $repository */ $repository = App::make('FireflyIII\Database\Recurring'); @@ -110,8 +111,8 @@ class GoogleTableController extends BaseController /** @var \RecurringTransaction $entry */ foreach ($set as $entry) { - $row = [$entry->id, route('recurring.edit', $entry->id), route('recurring.delete', $entry->id), route('recurring.show', $entry->id), $entry->name - , $entry->match,$entry->amount_min,$entry->amount_max + $row = [$entry->id, route('recurring.edit', $entry->id), route('recurring.delete', $entry->id), route('recurring.show', $entry->id), $entry->name, + $entry->match, $entry->amount_min, $entry->amount_max ]; $chart->addRowArray($row); @@ -136,99 +137,12 @@ class GoogleTableController extends BaseController return Response::json($chart->getData()); } - public function transactionsByRecurring(RecurringTransaction $recurring) { - /** @var \Grumpydictator\Gchart\GChart $chart */ - $chart = App::make('gchart'); - $chart->addColumn('ID', 'number'); - $chart->addColumn('ID_Edit', 'string'); - $chart->addColumn('ID_Delete', 'string'); - $chart->addColumn('Date', 'date'); - $chart->addColumn('Description_URL', 'string'); - $chart->addColumn('Description', 'string'); - $chart->addColumn('Amount', 'number'); - $chart->addColumn('From_URL', 'string'); - $chart->addColumn('From', 'string'); - $chart->addColumn('To_URL', 'string'); - $chart->addColumn('To', 'string'); - $chart->addColumn('Budget_URL', 'string'); - $chart->addColumn('Budget', 'string'); - $chart->addColumn('Category_URL', 'string'); - $chart->addColumn('Category', 'string'); - - $journals = $recurring->transactionjournals()->get(); - - /** @var TransactionJournal $transaction */ - foreach ($journals as $journal) { - $date = $journal->date; - $descriptionURL = route('transactions.show', $journal->id); - $description = $journal->description; - /** @var Transaction $transaction */ - foreach ($journal->transactions as $transaction) { - if (floatval($transaction->amount) > 0) { - $amount = floatval($transaction->amount); - $to = $transaction->account->name; - $toURL = route('accounts.show', $transaction->account->id); - } else { - $from = $transaction->account->name; - $fromURL = route('accounts.show', $transaction->account->id); - } - - } - if (isset($journal->budgets[0])) { - $budgetURL = route('budgets.show', $journal->budgets[0]->id); - $component = $journal->budgets[0]->name; - } else { - $budgetURL = ''; - $component = ''; - } - - if (isset($journal->categories[0])) { - $categoryURL = route('categories.show', $journal->categories[0]->id); - $category = $journal->categories[0]->name; - } else { - $categoryURL = ''; - $category = ''; - } - - - $id = $journal->id; - $edit = route('transactions.edit', $journal->id); - $delete = route('transactions.delete', $journal->id); - $chart->addRow( - $id, $edit, $delete, $date, $descriptionURL, $description, $amount, $fromURL, $from, $toURL, $to, $budgetURL, $component, $categoryURL, - $category - ); - } - - $chart->generate(); - - return Response::json($chart->getData()); - } - /** * @param Account $account */ public function transactionsByAccount(Account $account) { - /** @var \Grumpydictator\Gchart\GChart $chart */ - $chart = App::make('gchart'); - $chart->addColumn('ID', 'number'); - $chart->addColumn('ID_Edit', 'string'); - $chart->addColumn('ID_Delete', 'string'); - $chart->addColumn('Date', 'date'); - $chart->addColumn('Description_URL', 'string'); - $chart->addColumn('Description', 'string'); - $chart->addColumn('Amount', 'number'); - $chart->addColumn('From_URL', 'string'); - $chart->addColumn('From', 'string'); - $chart->addColumn('To_URL', 'string'); - $chart->addColumn('To', 'string'); - $chart->addColumn('Budget_URL', 'string'); - $chart->addColumn('Budget', 'string'); - $chart->addColumn('Category_URL', 'string'); - $chart->addColumn('Category', 'string'); - - + $table = new \FireflyIII\Shared\Google\Table\Transactions; /* * Find transactions: */ @@ -241,8 +155,10 @@ class GoogleTableController extends BaseController Session::get('start') )->orderBy('date', 'DESC')->get(); + $collection = new Collection; /** @var Transaction $transaction */ foreach ($transactions as $transaction) { + $date = $transaction->transactionJournal->date; $descriptionURL = route('transactions.show', $transaction->transaction_journal_id); $description = $transaction->transactionJournal->description; @@ -390,6 +306,18 @@ class GoogleTableController extends BaseController } + public function transactionsByRecurring(RecurringTransaction $recurring) + { + + /** @var \FireflyIII\Shared\Google\Table\Transactions $table */ + $table = new \FireflyIII\Shared\Google\Table\Transactions; + $journals = $recurring->transactionjournals()->get(); + + $table->addData($journals); + + return $table->generate(); + } + /** * @param $what * @@ -397,24 +325,21 @@ class GoogleTableController extends BaseController */ public function transactionsList($what) { - /** @var \Grumpydictator\Gchart\GChart $chart */ - $chart = App::make('gchart'); - $chart->addColumn('ID', 'number'); - $chart->addColumn('ID_Edit', 'string'); - $chart->addColumn('ID_Delete', 'string'); - $chart->addColumn('Date', 'date'); - $chart->addColumn('Description_URL', 'string'); - $chart->addColumn('Description', 'string'); - $chart->addColumn('Amount', 'number'); + /* + * Process some google stuff: + */ + $parameters = explode(' ',trim(Input::get('tq'))); + $limit = intval($parameters[1]); + $offset = intval($parameters[3]); + $request = explode(':',Input::get('tqx')); + $reqID = $request[1]; - $chart->addColumn('From_URL', 'string'); - $chart->addColumn('From', 'string'); - $chart->addColumn('To_URL', 'string'); - $chart->addColumn('To', 'string'); - $chart->addColumn('Budget_URL', 'string'); - $chart->addColumn('Budget', 'string'); - $chart->addColumn('Category_URL', 'string'); - $chart->addColumn('Category', 'string'); + /** @var \FireflyIII\Shared\Google\Table\Transactions $table */ + $table = new \FireflyIII\Shared\Google\Table\Transactions; + $table->setPaging(true); + $table->setLimit($limit); + $table->setOffset($offset); + $table->setReqID($reqID); /** @var \FireflyIII\Database\TransactionJournal $repository */ $repository = App::make('FireflyIII\Database\TransactionJournal'); @@ -422,72 +347,21 @@ class GoogleTableController extends BaseController switch ($what) { case 'expenses': case 'withdrawal': - $list = $repository->getWithdrawals(); + $list = $repository->getWithdrawals($limit, $offset); break; case 'revenue': case 'deposit': - $list = $repository->getDeposits(); + $list = $repository->getDeposits($limit, $offset); break; case 'transfer': case 'transfers': - $list = $repository->getTransfers(); + $list = $repository->getTransfers($limit, $offset); break; } - /** @var TransactionJournal $journal */ - foreach ($list as $journal) { - $date = $journal->date; - $descriptionURL = route('transactions.show', $journal->id); - $description = $journal->description; - $id = $journal->id; - if(!isset($journal->transactions[0]) || !isset($journal->transactions[1])) { - continue; - } + $table->addData($list); - - if ($journal->transactions[0]->amount < 0) { - - $fromURL = route('accounts.show', $journal->transactions[0]->account->id); - $fromName = $journal->transactions[0]->account->name; - $amount = floatval($journal->transactions[0]->amount); - - $toURL = route('accounts.show', $journal->transactions[1]->account->id); - $toName = $journal->transactions[1]->account->name; - - } else { - $fromURL = route('accounts.show', $journal->transactions[1]->account->id); - $fromName = $journal->transactions[1]->account->name; - $amount = floatval($journal->transactions[1]->amount); - - $toURL = route('accounts.show', $journal->transactions[0]->account->id); - $toName = $journal->transactions[0]->account->name; - } - if (isset($journal->budgets[0])) { - $budgetURL = route('budgets.show', $journal->budgets[0]->id); - $budget = $journal->budgets[0]->name; - } else { - $budgetURL = ''; - $budget = ''; - } - - if (isset($journal->categories[0])) { - $categoryURL = route('categories.show', $journal->categories[0]->id); - $category = $journal->categories[0]->name; - } else { - $categoryURL = ''; - $category = ''; - } - $edit = route('transactions.edit', $journal->id); - $delete = route('transactions.delete', $journal->id); - $chart->addRow( - $id, $edit, $delete, $date, $descriptionURL, $description, $amount, $fromURL, $fromName, $toURL, $toName, $budgetURL, $budget, $categoryURL, - $category - ); - } - - - $chart->generate(); - - return Response::json($chart->getData()); + echo $table->generate(); + exit; } } \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/TransactionJournal.php b/app/lib/FireflyIII/Database/TransactionJournal.php index c440e02405..07e13e7144 100644 --- a/app/lib/FireflyIII/Database/TransactionJournal.php +++ b/app/lib/FireflyIII/Database/TransactionJournal.php @@ -218,6 +218,7 @@ class TransactionJournal implements TransactionJournalInterface, CUD, CommonData } $transaction->save(); } + return new MessageBag; } @@ -509,9 +510,17 @@ class TransactionJournal implements TransactionJournalInterface, CUD, CommonData * * @return Collection */ - public function getDeposits() + public function getDeposits($limit = null, $offset = null) { - return $this->getUser()->transactionjournals()->withRelevantData()->transactionTypes(['Deposit'])->get(['transaction_journals.*']); + $query = $this->getUser()->transactionjournals()->withRelevantData()->transactionTypes(['Deposit']); + if(!is_null($limit)) { + $query->take($limit); + } + if(!is_null($offset) && intval($offset) > 0) { + $query->skip($offset); + } + + return $query->get(['transaction_journals.*']); } /** @@ -542,9 +551,17 @@ class TransactionJournal implements TransactionJournalInterface, CUD, CommonData * * @return Collection */ - public function getTransfers() + public function getTransfers($limit = null, $offset = null) { - return $this->getUser()->transactionjournals()->withRelevantData()->transactionTypes(['Transfer'])->get(['transaction_journals.*']); + $query = $this->getUser()->transactionjournals()->withRelevantData()->transactionTypes(['Transfer']); + + if(!is_null($limit)) { + $query->take($limit); + } + if(!is_null($offset) && intval($offset) > 0) { + $query->skip($offset); + } + return $query->get(['transaction_journals.*']); } /** @@ -552,8 +569,16 @@ class TransactionJournal implements TransactionJournalInterface, CUD, CommonData * * @return Collection */ - public function getWithdrawals() + public function getWithdrawals($limit = null, $offset = null) { - return $this->getUser()->transactionjournals()->withRelevantData()->transactionTypes(['Withdrawal'])->get(['transaction_journals.*']); + $query = $this->getUser()->transactionjournals()->withRelevantData()->transactionTypes(['Withdrawal']); + + if(!is_null($limit) && intval($limit) > 0) { + $query->take($limit); + } + if(!is_null($offset) && intval($offset) > 0) { + $query->skip($offset); + } + return $query->get(['transaction_journals.*']); } } \ No newline at end of file diff --git a/app/lib/FireflyIII/Shared/Google/Table.php b/app/lib/FireflyIII/Shared/Google/Table.php new file mode 100644 index 0000000000..1290430cb7 --- /dev/null +++ b/app/lib/FireflyIII/Shared/Google/Table.php @@ -0,0 +1,8 @@ +chart = \App::make('gchart'); + $this->chart->addColumn('ID', 'number'); + $this->chart->addColumn('ID_Edit', 'string'); + $this->chart->addColumn('ID_Delete', 'string'); + $this->chart->addColumn('Date', 'date'); + $this->chart->addColumn('Description_URL', 'string'); + $this->chart->addColumn('Description', 'string'); + $this->chart->addColumn('Amount', 'number'); + $this->chart->addColumn('From_URL', 'string'); + $this->chart->addColumn('From', 'string'); + $this->chart->addColumn('To_URL', 'string'); + $this->chart->addColumn('To', 'string'); + $this->chart->addColumn('Budget_URL', 'string'); + $this->chart->addColumn('Budget', 'string'); + $this->chart->addColumn('Category_URL', 'string'); + $this->chart->addColumn('Category', 'string'); + } + + public function addData(Collection $data) + { + /** @var \TransactionJournal $entry */ + foreach ($data as $entry) { + $date = $entry->date; + $descriptionURL = route('transactions.show', $entry->id); + $description = $entry->description; + /** @var Transaction $transaction */ + foreach ($entry->transactions as $transaction) { + if (floatval($transaction->amount) > 0) { + $amount = floatval($transaction->amount); + $to = $transaction->account->name; + $toURL = route('accounts.show', $transaction->account->id); + } else { + $from = $transaction->account->name; + $fromURL = route('accounts.show', $transaction->account->id); + } + + } + if (isset($entry->budgets[0])) { + $budgetURL = route('budgets.show', $entry->budgets[0]->id); + $component = $entry->budgets[0]->name; + } else { + $budgetURL = ''; + $component = ''; + } + + if (isset($entry->categories[0])) { + $categoryURL = route('categories.show', $entry->categories[0]->id); + $category = $entry->categories[0]->name; + } else { + $categoryURL = ''; + $category = ''; + } + + + $id = $entry->id; + $edit = route('transactions.edit', $entry->id); + $delete = route('transactions.delete', $entry->id); + $this->chart->addRow( + $id, $edit, $delete, $date, $descriptionURL, $description, $amount, $fromURL, $from, $toURL, $to, $budgetURL, $component, $categoryURL, + $category + ); + } + } + + public function generate() + { + if ($this->getPaging() && (is_null($this->getLimit()) || is_null($this->getOffset()) || is_null($this->getReqID()))) { + throw new FireflyException('Cannot page without parameters!'); + } + $this->chart->generate(); + if($this->getPaging()) { + $data = [ + 'version' => '0.6', + 'reqId' => $this->getReqID(), + 'status' => 'warning', + 'warnings' => [ + [ + 'reason' => 'data_truncated', + 'message' => 'Retrieved data was truncated', + 'detailed_message' => 'Data has been truncated due to userrequest (LIMIT in query)' + ] + ], + 'sig' => '12345', + 'table' => $this->chart->getData() + ]; + $return = '// Data table response'."\n".'google.visualization.Query.setResponse(' . json_encode($data).');'; + return $return; + //"version":"0.6","reqId":"0","status":"warning","warnings":[{"reason":"data_truncated","message":"Retrieved data was truncated","detailed_message":"Data has been truncated due to userrequest (LIMIT in query)"}],"sig":"253683512","table + } + + + + return \Response::json($this->chart->getData()); + } + + /** + * @return mixed + */ + public function getPaging() + { + return $this->paging; + } + + /** + * @param mixed $paging + */ + public function setPaging($paging) + { + $this->paging = $paging; + } + + /** + * @return int + */ + public function getLimit() + { + return $this->limit; + } + + /** + * @param int $limit + */ + public function setLimit($limit) + { + $this->limit = $limit; + } + + /** + * @return int + */ + public function getOffset() + { + return $this->offset; + } + + /** + * @param int $offset + */ + public function setOffset($offset) + { + $this->offset = $offset; + } + + /** + * @return string + */ + public function getReqID() + { + return $this->reqID; + } + + /** + * @param string $reqID + */ + public function setReqID($reqID) + { + $this->reqID = $reqID; + } + + +} \ No newline at end of file diff --git a/app/views/layouts/default.blade.php b/app/views/layouts/default.blade.php index 346742569b..94183c6c4b 100644 --- a/app/views/layouts/default.blade.php +++ b/app/views/layouts/default.blade.php @@ -17,7 +17,7 @@ {{HTML::style('assets/stylesheets/metisMenu/metisMenu.min.css')}} {{HTML::style('assets/stylesheets/sbadmin/sb.css')}} {{HTML::style('assets/stylesheets/fa/css/font-awesome.min.css')}} - {{HTML::style('http://fonts.googleapis.com/css?family=Roboto2')}} + {{HTML::style('https://fonts.googleapis.com/css?family=Roboto2')}} @yield('styles') +
@@ -34,6 +21,7 @@ var what = '{{{$what}}}'; +{{HTML::script('assets/javascript/google/TableQueryWrapper.js')}} {{HTML::script('assets/javascript/firefly/gcharts.options.js')}} {{HTML::script('assets/javascript/firefly/gcharts.js')}} diff --git a/public/assets/javascript/firefly/gcharts.js b/public/assets/javascript/firefly/gcharts.js index aec0f44a7a..bfac59511e 100644 --- a/public/assets/javascript/firefly/gcharts.js +++ b/public/assets/javascript/firefly/gcharts.js @@ -314,4 +314,21 @@ function googleTable(URL, container) { } else { console.log('No container found called "' + container + '"'); } +} + +/** + * + * @param URL + * @param container + */ +function googleTablePaged(URL, container) { + var query, options; + query = new google.visualization.Query(URL); + objContainer = document.getElementById(container); + options = {'pageSize': 5}; + + query.abort(); + var tableQueryWrapper = new TableQueryWrapper(query, objContainer, options); + tableQueryWrapper.sendAndDraw(); + } \ No newline at end of file diff --git a/public/assets/javascript/firefly/transactions.js b/public/assets/javascript/firefly/transactions.js index 7f65416544..5719db469c 100644 --- a/public/assets/javascript/firefly/transactions.js +++ b/public/assets/javascript/firefly/transactions.js @@ -15,7 +15,7 @@ if ($('input[name="category"]').length > 0) { } $(document).ready(function () { - if(typeof googleTable != 'undefined') { - googleTable('table/transactions/' + what,'transaction-table'); + if(typeof googleTablePaged != 'undefined') { + googleTablePaged('table/transactions/' + what,'transaction-table'); } }); \ No newline at end of file diff --git a/public/assets/javascript/google/TableQueryWrapper.js b/public/assets/javascript/google/TableQueryWrapper.js new file mode 100644 index 0000000000..381b3da886 --- /dev/null +++ b/public/assets/javascript/google/TableQueryWrapper.js @@ -0,0 +1,159 @@ +/** + * A wrapper for a query and a table visualization. + * The object only requests 1 page + 1 row at a time, by default, in order + * to minimize the amount of data held locally. + * Table sorting and pagination is executed by issuing + * additional requests with appropriate query parameters. + * E.g., for getting the data sorted by column 'A' the following query is + * attached to the request: 'tq=order by A'. + * + * Note: Discards query strings set by the user on the query object using + * google.visualization.Query#setQuery. + * + * DISCLAIMER: This is an example code which you can copy and change as + * required. It is used with the google visualization API table visualization + * which is assumed to be loaded to the page. For more info see: + * https://developers.google.com/chart/interactive/docs/gallery/table + * https://developers.google.com/chart/interactive/docs/reference#Query + */ + + +/** + * Constructs a new table query wrapper for the specified query, container + * and tableOptions. + * + * Note: The wrapper clones the options object to adjust some of its properties. + * In particular: + * sort {string} set to 'event'. + * page {string} set to 'event'. + * pageSize {Number} If number <= 0 set to 10. + * showRowNumber {boolean} set to true. + * firstRowNumber {number} set according to the current page. + * sortAscending {boolean} set according to the current sort. + * sortColumn {number} set according to the given sort. + * @constructor + */ +var TableQueryWrapper = function(query, container, options) { + + this.table = new google.visualization.Table(container); + this.query = query; + this.sortQueryClause = ''; + this.pageQueryClause = ''; + this.container = container; + this.currentDataTable = null; + + var self = this; + var addListener = google.visualization.events.addListener; + addListener(this.table, 'page', function(e) {self.handlePage(e)}); + addListener(this.table, 'sort', function(e) {self.handleSort(e)}); + + options = options || {}; + options = TableQueryWrapper.clone(options); + + options['sort'] = 'event'; + options['page'] = 'event'; + options['showRowNumber'] = true; + var buttonConfig = 'pagingButtonsConfiguration'; + options[buttonConfig] = options[buttonConfig] || 'both'; + options['pageSize'] = (options['pageSize'] > 0) ? options['pageSize'] : 10; + this.pageSize = options['pageSize']; + this.tableOptions = options; + this.currentPageIndex = 0; + this.setPageQueryClause(0); +}; + + +/** + * Sends the query and upon its return draws the Table visualization in the + * container. If the query refresh interval is set then the visualization will + * be redrawn upon each refresh. + */ +TableQueryWrapper.prototype.sendAndDraw = function() { + this.query.abort(); + var queryClause = this.sortQueryClause + ' ' + this.pageQueryClause; + this.query.setQuery(queryClause); + this.table.setSelection([]); + var self = this; + this.query.send(function(response) {self.handleResponse(response)}); +}; + + +/** Handles the query response after a send returned by the data source. */ +TableQueryWrapper.prototype.handleResponse = function(response) { + this.currentDataTable = null; + if (response.isError()) { + google.visualization.errors.addError(this.container, response.getMessage(), + response.getDetailedMessage(), {'showInTooltip': false}); + } else { + // make data: + //this.currentDataTable= new google.visualization.DataTable(response); + //console.log(response); + this.currentDataTable = response.getDataTable(); + this.table.draw(this.currentDataTable, this.tableOptions); + } +}; + + +/** Handles a sort event with the given properties. Will page to page=0. */ +TableQueryWrapper.prototype.handleSort = function(properties) { + var columnIndex = properties['column']; + var isAscending = properties['ascending']; + this.tableOptions['sortColumn'] = columnIndex; + this.tableOptions['sortAscending'] = isAscending; + // dataTable exists since the user clicked the table. + var colID = this.currentDataTable.getColumnId(columnIndex); + this.sortQueryClause = 'order by `' + colID + (!isAscending ? '` desc' : '`'); + // Calls sendAndDraw internally. + this.handlePage({'page': 0}); +}; + + +/** Handles a page event with the given properties. */ +TableQueryWrapper.prototype.handlePage = function(properties) { + var localTableNewPage = properties['page']; // 1, -1 or 0 + var newPage = 0; + if (localTableNewPage != 0) { + newPage = this.currentPageIndex + localTableNewPage; + } + if (this.setPageQueryClause(newPage)) { + this.sendAndDraw(); + } +}; + + +/** + * Sets the pageQueryClause and table options for a new page request. + * In case the next page is requested - checks that another page exists + * based on the previous request. + * Returns true if a new page query clause was set, false otherwise. + */ +TableQueryWrapper.prototype.setPageQueryClause = function(pageIndex) { + var pageSize = this.pageSize; + + if (pageIndex < 0) { + return false; + } + var dataTable = this.currentDataTable; + if ((pageIndex == this.currentPageIndex + 1) && dataTable) { + if (dataTable.getNumberOfRows() <= pageSize) { + return false; + } + } + this.currentPageIndex = pageIndex; + var newStartRow = this.currentPageIndex * pageSize; + // Get the pageSize + 1 so that we can know when the last page is reached. + this.pageQueryClause = 'limit ' + (pageSize + 1) + ' offset ' + newStartRow; + // Note: row numbers are 1-based yet dataTable rows are 0-based. + this.tableOptions['firstRowNumber'] = newStartRow + 1; + return true; +}; + + +/** Performs a shallow clone of the given object. */ +TableQueryWrapper.clone = function(obj) { + var newObj = {}; + for (var key in obj) { + newObj[key] = obj[key]; + } + return newObj; +}; \ No newline at end of file