1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 12:12:48 +01:00

Working on the dashboard

This commit is contained in:
Hillel Coren 2016-09-11 17:30:23 +03:00
parent c895cb2bbd
commit ce3a510037
29 changed files with 11017 additions and 1631 deletions

View File

@ -135,7 +135,8 @@ class CreateTestData extends Command
$data = [
'invoice_id' => $invoice->id,
'client_id' => $client->id,
'amount' => $this->faker->randomFloat(2, 0, $invoice->amount)
'amount' => $this->faker->randomFloat(2, 0, $invoice->amount),
'payment_date_sql' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),
];
$payment = $this->paymentRepo->save($data);

View File

@ -4,6 +4,8 @@ use stdClass;
use Auth;
use DB;
use View;
use Utils;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Payment;
use App\Ninja\Repositories\DashboardRepository;
@ -26,7 +28,8 @@ class DashboardController extends BaseController
$user = Auth::user();
$viewAll = $user->hasPermission('view_all');
$userId = $user->id;
$accountId = $user->account->id;
$account = $user->account;
$accountId = $account->id;
$dashboardRepo = $this->dashboardRepo;
$metrics = $dashboardRepo->totals($accountId, $userId, $viewAll);
@ -37,7 +40,10 @@ class DashboardController extends BaseController
$pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll);
$upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll);
$payments = $dashboardRepo->payments($accountId, $userId, $viewAll);
$expenses = $dashboardRepo->expenses($accountId, $userId, $viewAll);
$tasks = $dashboardRepo->tasks($accountId, $userId, $viewAll);
// check if the account has quotes
$hasQuotes = false;
foreach ([$upcoming, $pastDue] as $data) {
foreach ($data as $invoice) {
@ -47,6 +53,26 @@ class DashboardController extends BaseController
}
}
// check if the account has multiple curencies
$currencyIds = $account->currency_id ? [$account->currency_id] : [DEFAULT_CURRENCY];
$data = Client::scope()
->withArchived()
->distinct()
->get(['currency_id'])
->toArray();
array_map(function ($item) use (&$currencyIds) {
$currencyId = intval($item['currency_id']);
if ($currencyId && ! in_array($currencyId, $currencyIds)) {
$currencyIds[] = $currencyId;
}
}, $data);
$currencies = [];
foreach ($currencyIds as $currencyId) {
$currencies[$currencyId] = Utils::getFromCache($currencyId, 'currencies')->code;
}
$data = [
'account' => $user->account,
'paidToDate' => $paidToDate,
@ -60,8 +86,20 @@ class DashboardController extends BaseController
'payments' => $payments,
'title' => trans('texts.dashboard'),
'hasQuotes' => $hasQuotes,
'showBreadcrumbs' => false,
'currencies' => $currencies,
'expenses' => $expenses,
'tasks' => $tasks,
];
return View::make('dashboard', $data);
}
public function chartData($groupBy, $startDate, $endDate, $currencyCode, $includeExpenses)
{
$includeExpenses = filter_var($includeExpenses, FILTER_VALIDATE_BOOLEAN);
$data = $this->dashboardRepo->chartData(Auth::user()->account, $groupBy, $startDate, $endDate, $currencyCode, $includeExpenses);
return json_encode($data);
}
}

View File

@ -5,8 +5,6 @@ use Config;
use Input;
use Utils;
use DB;
use DateInterval;
use DatePeriod;
use Session;
use View;
use App\Models\Account;
@ -56,36 +54,17 @@ class ReportController extends BaseController
$action = Input::get('action');
if (Input::all()) {
$groupBy = Input::get('group_by');
$chartType = Input::get('chart_type');
$reportType = Input::get('report_type');
$dateField = Input::get('date_field');
$startDate = Utils::toSqlDate(Input::get('start_date'), false);
$endDate = Utils::toSqlDate(Input::get('end_date'), false);
$enableReport = boolval(Input::get('enable_report'));
$enableChart = boolval(Input::get('enable_chart'));
} else {
$groupBy = 'MONTH';
$chartType = 'Bar';
$reportType = ENTITY_INVOICE;
$dateField = FILTER_INVOICE_DATE;
$startDate = Utils::today(false)->modify('-3 month');
$endDate = Utils::today(false);
$enableReport = true;
$enableChart = true;
}
$dateTypes = [
'DAYOFYEAR' => 'Daily',
'WEEK' => 'Weekly',
'MONTH' => 'Monthly',
];
$chartTypes = [
'Bar' => 'Bar',
'Line' => 'Line',
];
$reportTypes = [
ENTITY_CLIENT => trans('texts.client'),
ENTITY_INVOICE => trans('texts.invoice'),
@ -96,148 +75,29 @@ class ReportController extends BaseController
];
$params = [
'dateTypes' => $dateTypes,
'chartTypes' => $chartTypes,
'chartType' => $chartType,
'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)),
'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)),
'groupBy' => $groupBy,
'reportTypes' => $reportTypes,
'reportType' => $reportType,
'enableChart' => $enableChart,
'enableReport' => $enableReport,
'title' => trans('texts.charts_and_reports'),
];
if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) {
if ($enableReport) {
$isExport = $action == 'export';
$params = array_merge($params, self::generateReport($reportType, $startDate, $endDate, $dateField, $isExport));
$isExport = $action == 'export';
$params = array_merge($params, self::generateReport($reportType, $startDate, $endDate, $dateField, $isExport));
if ($isExport) {
self::export($reportType, $params['displayData'], $params['columns'], $params['reportTotals']);
}
}
if ($enableChart) {
$params = array_merge($params, self::generateChart($groupBy, $startDate, $endDate));
if ($isExport) {
self::export($reportType, $params['displayData'], $params['columns'], $params['reportTotals']);
}
} else {
$params['columns'] = [];
$params['displayData'] = [];
$params['reportTotals'] = [];
$params['labels'] = [];
$params['datasets'] = [];
$params['scaleStepWidth'] = 100;
}
return View::make('reports.chart_builder', $params);
}
/**
* @param $groupBy
* @param $startDate
* @param $endDate
* @return array
*/
private function generateChart($groupBy, $startDate, $endDate)
{
$width = 10;
$datasets = [];
$labels = [];
$maxTotals = 0;
foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) {
// SQLite does not support the YEAR(), MONTH(), WEEK() and similar functions.
// Let's see if SQLite is being used.
if (Config::get('database.connections.'.Config::get('database.default').'.driver') == 'sqlite') {
// Replace the unsupported function with it's date format counterpart
switch ($groupBy) {
case 'MONTH':
$dateFormat = '%m'; // returns 01-12
break;
case 'WEEK':
$dateFormat = '%W'; // returns 00-53
break;
case 'DAYOFYEAR':
$dateFormat = '%j'; // returns 001-366
break;
default:
$dateFormat = '%m'; // MONTH by default
break;
}
// Concatenate the year and the chosen timeframe (Month, Week or Day)
$timeframe = 'strftime("%Y", '.$entityType.'_date) || strftime("'.$dateFormat.'", '.$entityType.'_date)';
} else {
// Supported by Laravel's other DBMS drivers (MySQL, MSSQL and PostgreSQL)
$timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))';
}
$records = DB::table($entityType.'s')
->select(DB::raw('sum('.$entityType.'s.amount) as total, '.$timeframe.' as '.$groupBy))
->join('clients', 'clients.id', '=', $entityType.'s.client_id')
->where('clients.is_deleted', '=', false)
->where($entityType.'s.account_id', '=', Auth::user()->account_id)
->where($entityType.'s.is_deleted', '=', false)
->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d'))
->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d'))
->groupBy($groupBy);
if ($entityType == ENTITY_INVOICE) {
$records->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false);
} elseif ($entityType == ENTITY_PAYMENT) {
$records->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('invoices.is_deleted', '=', false)
->whereNotIn('payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]);
}
$totals = $records->lists('total');
$dates = $records->lists($groupBy);
$data = array_combine($dates, $totals);
$padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month');
$endDate->modify('+1 '.$padding);
$interval = new DateInterval('P1'.substr($groupBy, 0, 1));
$period = new DatePeriod($startDate, $interval, $endDate);
$endDate->modify('-1 '.$padding);
$totals = [];
foreach ($period as $d) {
$dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n');
// MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365
$date = $groupBy == 'DAYOFYEAR' ? $d->format('Y').($d->format($dateFormat) + 1) : $d->format('Y'.$dateFormat);
$totals[] = isset($data[$date]) ? $data[$date] : 0;
if ($entityType == ENTITY_INVOICE) {
$labelFormat = $groupBy == 'DAYOFYEAR' ? 'j' : ($groupBy == 'WEEK' ? 'W' : 'F');
$label = $d->format($labelFormat);
$labels[] = $label;
}
}
$max = max($totals);
if ($max > 0) {
$datasets[] = [
'totals' => $totals,
'colors' => $entityType == ENTITY_INVOICE ? '78,205,196' : ($entityType == ENTITY_CREDIT ? '199,244,100' : '255,107,107'),
];
$maxTotals = max($max, $maxTotals);
}
}
$width = (ceil($maxTotals / 100) * 100) / 10;
$width = max($width, 10);
return [
'datasets' => $datasets,
'scaleStepWidth' => $width,
'labels' => $labels,
];
}
/**
* @param $reportType
* @param $startDate

View File

@ -18,8 +18,8 @@ class Kernel extends HttpKernel {
'App\Http\Middleware\VerifyCsrfToken',
'App\Http\Middleware\DuplicateSubmissionCheck',
'App\Http\Middleware\QueryLogging',
'App\Http\Middleware\StartupCheck',
'App\Http\Middleware\SessionDataCheckMiddleware',
'App\Http\Middleware\StartupCheck',
];

View File

@ -14,18 +14,16 @@ class SessionDataCheckMiddleware {
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next) {
public function handle($request, Closure $next)
{
$bag = Session::getMetadataBag();
$max = env('IDLE_TIMEOUT_MINUTES', 6 * 60) * 60; // minute to second conversion
$elapsed = time() - $bag->getLastUsed();
$max = config('session.lifetime') * 60; // minute to second conversion
if (($bag && $max < (time() - $bag->getLastUsed()))) {
$request->session()->flush(); // remove all the session data
Auth::logout(); // logout user
if ( ! $bag || $elapsed > $max) {
$request->session()->flush();
Auth::logout();
$request->session()->flash('warning', trans('texts.inactive_logout'));
}
return $next($request);

View File

@ -123,6 +123,7 @@ if (Utils::isReseller()) {
Route::group(['middleware' => 'auth:user'], function() {
Route::get('dashboard', 'DashboardController@index');
Route::get('dashboard_chart_data/{group_by}/{start_date}/{end_date}/{currency_id}/{include_expenses}', 'DashboardController@chartData');
Route::get('view_archive/{entity_type}/{visible}', 'AccountController@setTrashVisible');
Route::get('hide_message', 'HomeController@hideMessage');
Route::get('force_inline_pdf', 'UserController@forcePDFJS');
@ -238,8 +239,8 @@ Route::group([
Route::get('settings/email_preview', 'AccountController@previewEmail');
Route::get('company/{section}/{subSection?}', 'AccountController@redirectLegacy');
Route::get('settings/data_visualizations', 'ReportController@d3');
Route::get('settings/charts_and_reports', 'ReportController@showReports');
Route::post('settings/charts_and_reports', 'ReportController@showReports');
Route::get('settings/reports', 'ReportController@showReports');
Route::post('settings/reports', 'ReportController@showReports');
Route::post('settings/change_plan', 'AccountController@changePlan');
Route::post('settings/cancel_account', 'AccountController@cancelAccount');
@ -411,7 +412,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ACCOUNT_INVOICE_DESIGN', 'invoice_design');
define('ACCOUNT_CLIENT_PORTAL', 'client_portal');
define('ACCOUNT_EMAIL_SETTINGS', 'email_settings');
define('ACCOUNT_CHARTS_AND_REPORTS', 'charts_and_reports');
define('ACCOUNT_REPORTS', 'reports');
define('ACCOUNT_USER_MANAGEMENT', 'user_management');
define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations');
define('ACCOUNT_TEMPLATES_AND_REMINDERS', 'templates_and_reminders');

View File

@ -96,7 +96,7 @@ class Account extends Eloquent
ACCOUNT_TEMPLATES_AND_REMINDERS,
ACCOUNT_BANKS,
ACCOUNT_CLIENT_PORTAL,
ACCOUNT_CHARTS_AND_REPORTS,
ACCOUNT_REPORTS,
ACCOUNT_DATA_VISUALIZATIONS,
ACCOUNT_API_TOKENS,
ACCOUNT_USER_MANAGEMENT,
@ -401,11 +401,7 @@ class Account extends Eloquent
}
}
/**
* @param string $date
* @return DateTime|null|string
*/
public function getDateTime($date = 'now')
public function getDate($date = 'now')
{
if ( ! $date) {
return null;
@ -413,6 +409,16 @@ class Account extends Eloquent
$date = new \DateTime($date);
}
return $date;
}
/**
* @param string $date
* @return DateTime|null|string
*/
public function getDateTime($date = 'now')
{
$date = $this->getDate($date);
$date->setTimeZone(new \DateTimeZone($this->getTimezone()));
return $date;
@ -469,7 +475,7 @@ class Account extends Eloquent
*/
public function formatDate($date)
{
$date = $this->getDateTime($date);
$date = $this->getDate($date);
if ( ! $date) {
return null;

View File

@ -156,6 +156,15 @@ class Task extends EntityModel
{
return '#' . $this->public_id;
}
public function getDisplayName()
{
if ($this->description) {
return mb_strimwidth($this->description, 0, 16, "...");
}
return '#' . $this->public_id;
}
}

View File

@ -597,8 +597,11 @@ class BasePaymentDriver
$term = strtolower($matches[2]);
$price = $invoice_item->cost;
if ($plan == PLAN_ENTERPRISE) {
preg_match('/###[\d] [\w]* (\d*)/', $invoice_item->notes, $matches);
$numUsers = $matches[1];
if (count($matches)) {
$numUsers = $matches[1];
} else {
$numUsers = 5;
}
} else {
$numUsers = 1;
}

View File

@ -1,10 +1,148 @@
<?php namespace App\Ninja\Repositories;
use stdClass;
use DB;
use App\Models\Activity;
use App\Models\Invoice;
use App\Models\Task;
use DateInterval;
use DatePeriod;
class DashboardRepository
{
/**
* @param $groupBy
* @param $startDate
* @param $endDate
* @return array
*/
public function chartData($account, $groupBy, $startDate, $endDate, $currencyId, $includeExpenses)
{
$accountId = $account->id;
$startDate = date_create($startDate);
$endDate = date_create($endDate);
$groupBy = strtoupper($groupBy);
if ($groupBy == 'DAY') {
$groupBy = 'DAYOFYEAR';
}
$datasets = [];
$labels = [];
$totals = new stdClass;
$entitTypes = [ENTITY_INVOICE, ENTITY_PAYMENT];
if ($includeExpenses) {
$entitTypes[] = ENTITY_EXPENSE;
}
foreach ($entitTypes as $entityType) {
$data = [];
$count = 0;
$records = $this->rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId);
array_map(function ($item) use (&$data, &$count, $groupBy) {
$data[$item->$groupBy] = $item->total;
$count += $item->count;
}, $records->get());
$padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month');
$endDate->modify('+1 '.$padding);
$interval = new DateInterval('P1'.substr($groupBy, 0, 1));
$period = new DatePeriod($startDate, $interval, $endDate);
$endDate->modify('-1 '.$padding);
$records = [];
foreach ($period as $d) {
$dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n');
// MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365
$date = $groupBy == 'DAYOFYEAR' ? $d->format('Y').($d->format($dateFormat) + 1) : $d->format('Y'.$dateFormat);
$records[] = isset($data[$date]) ? $data[$date] : 0;
if ($entityType == ENTITY_INVOICE) {
$labels[] = $d->format('r');
}
}
if ($entityType == ENTITY_INVOICE) {
$color = '51,122,183';
} elseif ($entityType == ENTITY_PAYMENT) {
$color = '54,193,87';
} elseif ($entityType == ENTITY_EXPENSE) {
$color = '128,128,128';
}
$record = new stdClass;
$record->data = $records;
$record->label = trans("texts.{$entityType}s");
$record->lineTension = 0;
$record->borderWidth = 4;
$record->borderColor = "rgba({$color}, 1)";
$record->backgroundColor = "rgba({$color}, 0.05)";
$datasets[] = $record;
if ($entityType == ENTITY_INVOICE) {
$totals->invoices = array_sum($data);
$totals->average = $count ? round($totals->invoices / $count, 2) : 0;
} elseif ($entityType == ENTITY_PAYMENT) {
$totals->revenue = array_sum($data);
$totals->balance = $totals->invoices - $totals->revenue;
} elseif ($entityType == ENTITY_EXPENSE) {
//$totals->profit = $totals->revenue - array_sum($data);
$totals->expenses = array_sum($data);
}
}
$data = new stdClass;
$data->labels = $labels;
$data->datasets = $datasets;
$response = new stdClass;
$response->data = $data;
$response->totals = $totals;
return $response;
}
private function rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId)
{
$accountId = $account->id;
$currencyId = intval($currencyId);
$timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))';
$records = DB::table($entityType.'s')
->join('clients', 'clients.id', '=', $entityType.'s.client_id')
->where('clients.is_deleted', '=', false)
->where($entityType.'s.account_id', '=', $accountId)
->where($entityType.'s.is_deleted', '=', false)
->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d'))
->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d'))
->groupBy($groupBy);
if ($entityType == ENTITY_EXPENSE) {
$records->where('expenses.expense_currency_id', '=', $currencyId);
} elseif ($currencyId == $account->getCurrencyId()) {
$records->whereRaw("(clients.currency_id = {$currencyId} or coalesce(clients.currency_id, 0) = 0)");
} else {
$records->where('clients.currency_id', '=', $currencyId);
}
if ($entityType == ENTITY_INVOICE) {
$records->select(DB::raw('sum(invoices.amount) as total, count(invoices.id) as count, '.$timeframe.' as '.$groupBy))
->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false);
} elseif ($entityType == ENTITY_PAYMENT) {
$records->select(DB::raw('sum(payments.amount - payments.refunded) as total, count(payments.id) as count, '.$timeframe.' as '.$groupBy))
->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('invoices.is_deleted', '=', false)
->whereNotIn('payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]);
} elseif ($entityType == ENTITY_EXPENSE) {
$records->select(DB::raw('sum(expenses.amount) as total, count(expenses.id) as count, '.$timeframe.' as '.$groupBy));
}
return $records;
}
public function totals($accountId, $userId, $viewAll)
{
// total_income, billed_clients, invoice_sent and active_clients
@ -193,4 +331,33 @@ class DashboardRepository
->take(50)
->get();
}
public function expenses($accountId, $userId, $viewAll)
{
$select = DB::raw(
'SUM('.DB::getQueryGrammar()->wrap('expenses.amount', true).') as value,'
.DB::getQueryGrammar()->wrap('expenses.expense_currency_id', true).' as currency_id'
);
$paidToDate = DB::table('accounts')
->select($select)
->leftJoin('expenses', 'accounts.id', '=', 'expenses.account_id')
->where('accounts.id', '=', $accountId)
->where('expenses.is_deleted', '=', false);
if (!$viewAll){
$paidToDate = $paidToDate->where('expenses.user_id', '=', $userId);
}
return $paidToDate->groupBy('accounts.id')
->groupBy('expenses.expense_currency_id')
->get();
}
public function tasks($accountId, $userId, $viewAll)
{
return Task::scope()
->withArchived()
->whereIsRunning(true)
->get();
}
}

View File

@ -29,7 +29,8 @@
"stacktrace-js": "~1.0.1",
"fuse.js": "~2.0.2",
"dropzone": "~4.3.0",
"sweetalert": "~1.1.3"
"sweetalert": "~1.1.3",
"bootstrap-daterangepicker": "~2.1.24"
},
"resolutions": {
"jquery": "~1.11"

View File

@ -29,7 +29,7 @@ return [
|
*/
'lifetime' => env('SESSION_LIFETIME', 120),
'lifetime' => env('SESSION_LIFETIME', (60 * 8)),
'expire_on_close' => false,

View File

@ -58,6 +58,11 @@ elixir(function(mix) {
'fonts.css'
], 'public/css/built.css');
mix.styles([
bowerDir + '/bootstrap-daterangepicker/daterangepicker.css'
], 'public/css/daterangepicker.css');
/**
* JS configuration
*/
@ -71,6 +76,10 @@ elixir(function(mix) {
'vfs.js'
], 'public/pdf.built.js');
mix.scripts([
bowerDir + '/bootstrap-daterangepicker/daterangepicker.js'
], 'public/js/daterangepicker.min.js');
mix.scripts([
bowerDir + '/jquery/dist/jquery.js',
bowerDir + '/jquery-ui/jquery-ui.js',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/css/daterangepicker.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
public/js/daterangepicker.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -100,8 +100,8 @@
#right-sidebar-wrapper .sidebar-nav li {
text-indent: 8px;
font-size: 16px;
line-height: 44px;
font-size: 15px;
line-height: 42px;
}
#right-sidebar-wrapper .sidebar-nav li a.btn {

File diff suppressed because it is too large Load Diff

View File

@ -2099,6 +2099,15 @@ $LANG = array(
'facebook_and_twitter_help' => 'Follow our feeds to help support our project',
'reseller_text' => 'Note: the white-label license is intended for personal use, please email us at :email if you\'d like to resell our app.',
'unnamed_client' => 'Unnamed Client',
'day' => 'Day',
'week' => 'Week',
'month' => 'Month',
'inactive_logout' => 'You have been logged out due to inactivity',
'reports' => 'Reports',
'total_profit' => 'Total Profit',
'total_expenses' => 'Total Expenses',
);
return $LANG;

View File

@ -1,22 +1,214 @@
@extends('header')
@section('head')
@parent
@include('money_script')
<script src="{!! asset('js/Chart.min.js') !!}" type="text/javascript"></script>
<script src="{{ asset('js/daterangepicker.min.js') }}" type="text/javascript"></script>
<link href="{{ asset('css/daterangepicker.css') }}" rel="stylesheet" type="text/css"/>
@stop
@section('content')
<script type="text/javascript">
@if (Auth::user()->hasPermission('view_all'))
function loadChart(data) {
var ctx = document.getElementById('chart-canvas').getContext('2d');
if (window.myChart) {
window.myChart.config.data = data;
window.myChart.config.options.scales.xAxes[0].time.unit = chartGropuBy.toLowerCase();
window.myChart.update();
} else {
$('#progress-div').hide();
$('#chart-canvas').fadeIn();
window.myChart = new Chart(ctx, {
type: 'line',
data: data,
options: {
tooltips: {
mode: 'x-axis',
titleFontSize: 15,
titleMarginBottom: 12,
bodyFontSize: 15,
bodySpacing: 10,
callbacks: {
title: function(item) {
return moment(item[0].xLabel).format("{{ $account->getMomentDateFormat() }}");
},
label: function(item, data) {
if (item.datasetIndex == 0) {
var label = " {{ trans('texts.invoices') }}: ";
} else if (item.datasetIndex == 1) {
var label = " {{ trans('texts.payments') }}: ";
} else if (item.datasetIndex == 2) {
var label = " {{ trans('texts.expenses') }}: ";
}
return label + formatMoney(item.yLabel, chartCurrencyId, account.country_id);
}
}
},
title: {
display: false,
fontSize: 18,
text: '{{ trans('texts.total_revenue') }}'
},
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'day',
},
gridLines: {
display: false,
},
}],
yAxes: [{
ticks: {
beginAtZero: true,
callback: function(label, index, labels) {
return formatMoney(label, chartCurrencyId, account.country_id);
}
},
}]
}
}
});
}
}
var account = {!! $account !!};
var chartStartDate = moment().subtract(29, 'days');
var chartEndDate = moment();
var chartGropuBy = 'day';
var chartCurrencyId = {{ $account->getCurrencyId() }};
$(function() {
// Initialize date range selector
function cb(start, end) {
$('#reportrange span').html(start.format('{{ $account->getMomentDateFormat() }}') + ' - ' + end.format('{{ $account->getMomentDateFormat() }}'));
chartStartDate = start;
chartEndDate = end;
loadData();
}
$('#reportrange').daterangepicker({
locale: {
"format": "{{ $account->getMomentDateFormat() }}",
},
startDate: chartStartDate,
endDate: chartEndDate,
linkedCalendars: false,
ranges: {
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
'This Month': [moment().startOf('month'), moment().endOf('month')],
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
}
}, cb);
cb(chartStartDate, chartEndDate);
$("#currency-btn-group > .btn").click(function(){
$(this).addClass("active").siblings().removeClass("active");
chartCurrencyId = currencyMap[$(this).text()].id;
loadData();
});
$("#group-btn-group > .btn").click(function(){
$(this).addClass("active").siblings().removeClass("active");
chartGropuBy = $(this).text();
loadData();
});
function loadData() {
var includeExpenses = "{{ count($expenses) ? 'true' : 'false' }}";
var url = "{!! url('/dashboard_chart_data') !!}/" + chartGropuBy + '/' + chartStartDate.format('YYYY-MM-DD') + '/' + chartEndDate.format('YYYY-MM-DD') + '/' + chartCurrencyId + '/' + includeExpenses;
$.get(url, function(response) {
response = JSON.parse(response);
loadChart(response.data);
var totals = response.totals;
$('.revenue-div').text(formatMoney(totals.revenue, chartCurrencyId, account.country_id));
$('.outstanding-div').text(formatMoney(totals.balance, chartCurrencyId, account.country_id));
$('.expenses-div').text(formatMoney(totals.expenses, chartCurrencyId, account.country_id));
$('.average-div').text(formatMoney(totals.average, chartCurrencyId, account.country_id));
$('.currency').hide();
$('.currency_' + chartCurrencyId).show();
})
}
});
@else
$(function() {
$('.currency').show();
})
@endif
</script>
<div class="row">
<div class="col-md-2">
<ol class="breadcrumb"><li class='active'>{{ trans('texts.dashboard') }}</li></ol>
</div>
@if (count($tasks))
<div class="col-md-2" style="padding-top:6px">
@foreach ($tasks as $task)
{!! Button::primary($task->present()->titledName)->small()->asLinkTo($task->present()->url) !!}
@endforeach
</div>
<div class="col-md-8">
@else
<div class="col-md-10">
@endif
@if (Auth::user()->hasPermission('view_all'))
<div class="pull-right">
@if (count($currencies) > 1)
<div id="currency-btn-group" class="btn-group" role="group" style="border: 1px solid #ccc;">
@foreach ($currencies as $key => $val)
<button type="button" class="btn btn-normal {{ array_values($currencies)[0] == $val ? 'active' : '' }}"
style="font-weight:normal !important;background-color:white">{{ $val }}</button>
@endforeach
</div>
@endif
<div id="group-btn-group" class="btn-group" role="group" style="border: 1px solid #ccc; margin-left:18px">
<button type="button" class="btn btn-normal active" style="font-weight:normal !important;background-color:white">{{ trans('texts.day') }}</button>
<button type="button" class="btn btn-normal" style="font-weight:normal !important;background-color:white">{{ trans('texts.week') }}</button>
<button type="button" class="btn btn-normal" style="font-weight:normal !important;background-color:white">{{ trans('texts.month') }}</button>
</div>
<div id="reportrange" class="pull-right" style="background: #fff; cursor: pointer; padding: 9px 14px; border: 1px solid #ccc; margin-top: 0px; margin-left:18px">
<i class="glyphicon glyphicon-calendar fa fa-calendar"></i>&nbsp;
<span></span> <b class="caret"></b>
</div>
</div>
@endif
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-body">
<img src="{{ asset('images/totalinvoices.png') }}"
class="in-image" style="float:left" width="80" height="80"/>
<div style="overflow:hidden">
<div class="in-thin">
{{ trans('texts.total_revenue') }}
</div>
<div class="revenue-div in-bold pull-right" style="color:#337ab7">
</div>
<div class="in-bold">
@if (count($paidToDate))
@foreach ($paidToDate as $item)
{{ Utils::formatMoney($item->value, $item->currency_id) }}<br/>
<div class="currency currency_{{ $item->currency_id ?: $account->getCurrencyId() }}" style="display:none">
{{ Utils::formatMoney($item->value, $item->currency_id) }}
</div>
@endforeach
@else
{{ Utils::formatMoney(0) }}
@ -29,21 +221,38 @@
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-body">
<img src="{{ asset('images/clients.png') }}"
class="in-image" style="float:left" width="80" height="80"/>
<div style="overflow:hidden">
<div class="in-thin">
{{ trans('texts.average_invoice') }}
</div>
<div class="in-bold">
@if (count($averageInvoice))
@foreach ($averageInvoice as $item)
{{ Utils::formatMoney($item->invoice_avg, $item->currency_id) }}<br/>
@if (count($expenses))
<div class="in-thin">
{{ trans('texts.total_expenses') }}
</div>
<div class="expenses-div in-bold pull-right" style="color:#337ab7">
</div>
<div class="in-bold">
@foreach ($expenses as $item)
<div class="currency currency_{{ $item->currency_id ?: $account->getCurrencyId() }}" style="display:none">
{{ Utils::formatMoney($item->value, $item->currency_id) }}<br/>
</div>
@endforeach
@else
{{ Utils::formatMoney(0) }}
@endif
</div>
</div>
@else
<div class="in-thin">
{{ trans('texts.average_invoice') }}
</div>
<div class="average-div in-bold pull-right" style="color:#337ab7">
</div>
<div class="in-bold">
@if (count($averageInvoice))
@foreach ($averageInvoice as $item)
<div class="currency currency_{{ $item->currency_id ?: $account->getCurrencyId() }}" style="display:none">
{{ Utils::formatMoney($item->invoice_avg, $item->currency_id) }}<br/>
</div>
@endforeach
@else
{{ Utils::formatMoney(0) }}
@endif
</div>
@endif
</div>
</div>
</div>
@ -51,16 +260,18 @@
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-body">
<img src="{{ asset('images/totalincome.png') }}"
class="in-image" style="float:left" width="80" height="80"/>
<div style="overflow:hidden">
<div class="in-thin">
{{ trans('texts.outstanding') }}
</div>
<div class="outstanding-div in-bold pull-right" style="color:#337ab7">
</div>
<div class="in-bold">
@if (count($balances))
@foreach ($balances as $item)
{{ Utils::formatMoney($item->value, $item->currency_id) }}<br/>
<div class="currency currency_{{ $item->currency_id ?: $account->getCurrencyId() }}" style="display:none">
{{ Utils::formatMoney($item->value, $item->currency_id) }}<br/>
</div>
@endforeach
@else
{{ Utils::formatMoney(0) }}
@ -72,13 +283,23 @@
</div>
</div>
@if (Auth::user()->hasPermission('view_all'))
<div class="row">
<div class="col-md-12">
<div id="progress-div" class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar"
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div>
</div>
<canvas id="chart-canvas" height="70px" style="background-color:white;padding:20px;display:none"></canvas>
</div>
</div>
<p>&nbsp;</p>
@endif
<div class="row">
<div class="col-md-6">
<div class="panel panel-default dashboard" style="height:320px">
<div class="panel-heading">
<div class="panel-heading" style="background-color:#286090 !important">
<h3 class="panel-title in-bold-white">
<i class="glyphicon glyphicon-exclamation-sign"></i> {{ trans('texts.activity') }}
@if ($invoicesSent)
@ -103,6 +324,17 @@
<div class="panel panel-default dashboard" style="height:320px;">
<div class="panel-heading" style="margin:0; background-color: #f5f5f5 !important;">
<h3 class="panel-title" style="color: black !important">
@if (count($expenses) && count($averageInvoice))
<div class="pull-right" style="font-size:14px;padding-top:4px;font-weight:bold">
@foreach ($averageInvoice as $item)
<span class="currency currency_{{ $item->currency_id ?: $account->getCurrencyId() }}" style="display:none">
{{ trans('texts.average_invoice') }}
{{ Utils::formatMoney($item->invoice_avg, $item->currency_id) }} |
</span>
@endforeach
<span class="average-div" style="color:#337ab7"/>
</div>
@endif
<i class="glyphicon glyphicon-ok-sign"></i> {{ trans('texts.recent_payments') }}
</h3>
</div>
@ -172,7 +404,7 @@
</div>
<div class="col-md-6">
<div class="panel panel-default dashboard" style="height:320px">
<div class="panel-heading" style="background-color:#e37329 !important">
<div class="panel-heading" style="background-color:#777 !important">
<h3 class="panel-title in-bold-white">
<i class="glyphicon glyphicon-time"></i> {{ trans('texts.invoices_past_due') }}
</h3>
@ -242,7 +474,7 @@
</div>
<div class="col-md-6">
<div class="panel panel-default dashboard" style="height:320px">
<div class="panel-heading" style="background-color:#e37329 !important">
<div class="panel-heading" style="background-color:#777 !important">
<h3 class="panel-title in-bold-white">
<i class="glyphicon glyphicon-time"></i> {{ trans('texts.expired_quotes') }}
</h3>

View File

@ -6,7 +6,6 @@
<link href="{{ asset('css/built.css') }}?no_cache={{ NINJA_VERSION }}" rel="stylesheet" type="text/css"/>
<style type="text/css">
@if (Auth::check() && Auth::user()->dark_mode)
body {
background: #000 !important;

View File

@ -5,6 +5,7 @@
for (var i=0; i<currencies.length; i++) {
var currency = currencies[i];
currencyMap[currency.id] = currency;
currencyMap[currency.code] = currency;
}
var countries = {!! \Cache::get('countries') !!};
@ -28,7 +29,7 @@
NINJA.parseFloat = function(str) {
if (!str) return '';
str = (str+'').replace(/[^0-9\.\-]/g, '');
return window.parseFloat(str);
}
@ -97,4 +98,4 @@
}
}
</script>
</script>

View File

@ -69,10 +69,14 @@
} else {
// response contains id and card, which contains additional card details
var token = response.id;
// Insert the token into the form so it gets submitted to the server
$form.append($('<input type="hidden" name="sourceToken"/>').val(token));
// and submit
$form.get(0).submit();
if (token) {
// Insert the token into the form so it gets submitted to the server
$form.append($('<input type="hidden" name="sourceToken"/>').val(token));
// and submit
$form.get(0).submit();
} else {
logError(JSON.stringify(response));
}
}
};
</script>

View File

@ -1,14 +1,8 @@
@extends('header')
@section('head')
@parent
<script src="{!! asset('js/Chart.min.js') !!}" type="text/javascript"></script>
@stop
@section('content')
@parent
@include('accounts.nav', ['selected' => ACCOUNT_CHARTS_AND_REPORTS, 'advanced' => true])
@include('accounts.nav', ['selected' => ACCOUNT_REPORTS, 'advanced' => true])
{!! Former::open()->rules(['start_date' => 'required', 'end_date' => 'required'])->addClass('warn-on-exit') !!}
@ -54,18 +48,12 @@
</div>
<div class="col-md-6">
{!! Former::checkbox('enable_report')->text(trans('texts.enable'))->check($enableReport)->forceValue(1) !!}
{!! Former::select('report_type')->options($reportTypes, $reportType)->label(trans('texts.type')) !!}
<div id="dateField" style="display:{{ $reportType == ENTITY_TAX_RATE ? 'block' : 'none' }}">
{!! Former::select('date_field')->label(trans('texts.filter'))
->addOption(trans('texts.invoice_date'), FILTER_INVOICE_DATE)
->addOption(trans('texts.payment_date'), FILTER_PAYMENT_DATE) !!}
</div>
<p>&nbsp;</p>
{!! Former::checkbox('enable_chart')->text(trans('texts.enable'))->check($enableChart)->forceValue(1) !!}
{!! Former::select('group_by')->options($dateTypes, $groupBy) !!}
{!! Former::select('chart_type')->options($chartTypes, $chartType) !!}
{!! Former::close() !!}
</div>
@ -73,7 +61,6 @@
</div>
</div>
@if ($enableReport)
<div class="panel panel-default">
<div class="panel-body">
<table class="table table-striped invoice-table">
@ -128,29 +115,6 @@
</div>
</div>
@endif
@if ($enableChart)
<div class="panel panel-default">
<div class="panel-body">
<canvas id="monthly-reports" width="700" height="400"></canvas>
<p>&nbsp;</p>
<div style="padding-bottom:8px">
<div style="float:left; height:22px; width:60px; background-color:rgba(78,205,196,.5); border: 1px solid rgba(78,205,196,1)"></div>
<div style="vertical-align: middle">&nbsp;Invoices</div>
</div>
<div style="padding-bottom:8px; clear:both">
<div style="float:left; height:22px; width:60px; background-color:rgba(255,107,107,.5); border: 1px solid rgba(255,107,107,1)"></div>
<div style="vertical-align: middle">&nbsp;Payments</div>
</div>
<div style="clear:both">
<div style="float:left; height:22px; width:60px; background-color:rgba(199,244,100,.5); border: 1px solid rgba(199,244,100,1)"></div>
<div style="vertical-align: middle">&nbsp;Credits</div>
</div>
</div>
</div>
@endif
</div>
@ -162,32 +126,6 @@
$('#action').val('');
}
@if ($enableChart)
var ctx = document.getElementById('monthly-reports').getContext('2d');
var chart = {
labels: {!! json_encode($labels) !!},
datasets: [
@foreach ($datasets as $dataset)
{
data: {!! json_encode($dataset['totals']) !!},
fillColor : "rgba({!! $dataset['colors'] !!},0.5)",
strokeColor : "rgba({!! $dataset['colors'] !!},1)",
},
@endforeach
]
}
var options = {
scaleOverride: true,
scaleSteps: 10,
scaleStepWidth: {!! $scaleStepWidth !!},
scaleStartValue: 0,
scaleLabel : "<%=value%>",
};
new Chart(ctx).{!! $chartType !!}(chart, options);
@endif
$(function() {
$('.start_date .input-group-addon').click(function() {
toggleDatePicker('start_date');