Working on the dashboard
This commit is contained in:
parent
c895cb2bbd
commit
ce3a510037
|
|
@ -135,7 +135,8 @@ class CreateTestData extends Command
|
||||||
$data = [
|
$data = [
|
||||||
'invoice_id' => $invoice->id,
|
'invoice_id' => $invoice->id,
|
||||||
'client_id' => $client->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);
|
$payment = $this->paymentRepo->save($data);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ use stdClass;
|
||||||
use Auth;
|
use Auth;
|
||||||
use DB;
|
use DB;
|
||||||
use View;
|
use View;
|
||||||
|
use Utils;
|
||||||
|
use App\Models\Client;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
use App\Ninja\Repositories\DashboardRepository;
|
use App\Ninja\Repositories\DashboardRepository;
|
||||||
|
|
@ -26,7 +28,8 @@ class DashboardController extends BaseController
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$viewAll = $user->hasPermission('view_all');
|
$viewAll = $user->hasPermission('view_all');
|
||||||
$userId = $user->id;
|
$userId = $user->id;
|
||||||
$accountId = $user->account->id;
|
$account = $user->account;
|
||||||
|
$accountId = $account->id;
|
||||||
|
|
||||||
$dashboardRepo = $this->dashboardRepo;
|
$dashboardRepo = $this->dashboardRepo;
|
||||||
$metrics = $dashboardRepo->totals($accountId, $userId, $viewAll);
|
$metrics = $dashboardRepo->totals($accountId, $userId, $viewAll);
|
||||||
|
|
@ -37,7 +40,10 @@ class DashboardController extends BaseController
|
||||||
$pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll);
|
$pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll);
|
||||||
$upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll);
|
$upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll);
|
||||||
$payments = $dashboardRepo->payments($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;
|
$hasQuotes = false;
|
||||||
foreach ([$upcoming, $pastDue] as $data) {
|
foreach ([$upcoming, $pastDue] as $data) {
|
||||||
foreach ($data as $invoice) {
|
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 = [
|
$data = [
|
||||||
'account' => $user->account,
|
'account' => $user->account,
|
||||||
'paidToDate' => $paidToDate,
|
'paidToDate' => $paidToDate,
|
||||||
|
|
@ -60,8 +86,20 @@ class DashboardController extends BaseController
|
||||||
'payments' => $payments,
|
'payments' => $payments,
|
||||||
'title' => trans('texts.dashboard'),
|
'title' => trans('texts.dashboard'),
|
||||||
'hasQuotes' => $hasQuotes,
|
'hasQuotes' => $hasQuotes,
|
||||||
|
'showBreadcrumbs' => false,
|
||||||
|
'currencies' => $currencies,
|
||||||
|
'expenses' => $expenses,
|
||||||
|
'tasks' => $tasks,
|
||||||
];
|
];
|
||||||
|
|
||||||
return View::make('dashboard', $data);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ use Config;
|
||||||
use Input;
|
use Input;
|
||||||
use Utils;
|
use Utils;
|
||||||
use DB;
|
use DB;
|
||||||
use DateInterval;
|
|
||||||
use DatePeriod;
|
|
||||||
use Session;
|
use Session;
|
||||||
use View;
|
use View;
|
||||||
use App\Models\Account;
|
use App\Models\Account;
|
||||||
|
|
@ -56,36 +54,17 @@ class ReportController extends BaseController
|
||||||
$action = Input::get('action');
|
$action = Input::get('action');
|
||||||
|
|
||||||
if (Input::all()) {
|
if (Input::all()) {
|
||||||
$groupBy = Input::get('group_by');
|
|
||||||
$chartType = Input::get('chart_type');
|
|
||||||
$reportType = Input::get('report_type');
|
$reportType = Input::get('report_type');
|
||||||
$dateField = Input::get('date_field');
|
$dateField = Input::get('date_field');
|
||||||
$startDate = Utils::toSqlDate(Input::get('start_date'), false);
|
$startDate = Utils::toSqlDate(Input::get('start_date'), false);
|
||||||
$endDate = Utils::toSqlDate(Input::get('end_date'), false);
|
$endDate = Utils::toSqlDate(Input::get('end_date'), false);
|
||||||
$enableReport = boolval(Input::get('enable_report'));
|
|
||||||
$enableChart = boolval(Input::get('enable_chart'));
|
|
||||||
} else {
|
} else {
|
||||||
$groupBy = 'MONTH';
|
|
||||||
$chartType = 'Bar';
|
|
||||||
$reportType = ENTITY_INVOICE;
|
$reportType = ENTITY_INVOICE;
|
||||||
$dateField = FILTER_INVOICE_DATE;
|
$dateField = FILTER_INVOICE_DATE;
|
||||||
$startDate = Utils::today(false)->modify('-3 month');
|
$startDate = Utils::today(false)->modify('-3 month');
|
||||||
$endDate = Utils::today(false);
|
$endDate = Utils::today(false);
|
||||||
$enableReport = true;
|
|
||||||
$enableChart = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$dateTypes = [
|
|
||||||
'DAYOFYEAR' => 'Daily',
|
|
||||||
'WEEK' => 'Weekly',
|
|
||||||
'MONTH' => 'Monthly',
|
|
||||||
];
|
|
||||||
|
|
||||||
$chartTypes = [
|
|
||||||
'Bar' => 'Bar',
|
|
||||||
'Line' => 'Line',
|
|
||||||
];
|
|
||||||
|
|
||||||
$reportTypes = [
|
$reportTypes = [
|
||||||
ENTITY_CLIENT => trans('texts.client'),
|
ENTITY_CLIENT => trans('texts.client'),
|
||||||
ENTITY_INVOICE => trans('texts.invoice'),
|
ENTITY_INVOICE => trans('texts.invoice'),
|
||||||
|
|
@ -96,148 +75,29 @@ class ReportController extends BaseController
|
||||||
];
|
];
|
||||||
|
|
||||||
$params = [
|
$params = [
|
||||||
'dateTypes' => $dateTypes,
|
|
||||||
'chartTypes' => $chartTypes,
|
|
||||||
'chartType' => $chartType,
|
|
||||||
'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)),
|
'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)),
|
||||||
'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)),
|
'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)),
|
||||||
'groupBy' => $groupBy,
|
|
||||||
'reportTypes' => $reportTypes,
|
'reportTypes' => $reportTypes,
|
||||||
'reportType' => $reportType,
|
'reportType' => $reportType,
|
||||||
'enableChart' => $enableChart,
|
|
||||||
'enableReport' => $enableReport,
|
|
||||||
'title' => trans('texts.charts_and_reports'),
|
'title' => trans('texts.charts_and_reports'),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) {
|
if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) {
|
||||||
if ($enableReport) {
|
$isExport = $action == 'export';
|
||||||
$isExport = $action == 'export';
|
$params = array_merge($params, self::generateReport($reportType, $startDate, $endDate, $dateField, $isExport));
|
||||||
$params = array_merge($params, self::generateReport($reportType, $startDate, $endDate, $dateField, $isExport));
|
|
||||||
|
|
||||||
if ($isExport) {
|
if ($isExport) {
|
||||||
self::export($reportType, $params['displayData'], $params['columns'], $params['reportTotals']);
|
self::export($reportType, $params['displayData'], $params['columns'], $params['reportTotals']);
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($enableChart) {
|
|
||||||
$params = array_merge($params, self::generateChart($groupBy, $startDate, $endDate));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$params['columns'] = [];
|
$params['columns'] = [];
|
||||||
$params['displayData'] = [];
|
$params['displayData'] = [];
|
||||||
$params['reportTotals'] = [];
|
$params['reportTotals'] = [];
|
||||||
$params['labels'] = [];
|
|
||||||
$params['datasets'] = [];
|
|
||||||
$params['scaleStepWidth'] = 100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return View::make('reports.chart_builder', $params);
|
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 $reportType
|
||||||
* @param $startDate
|
* @param $startDate
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ class Kernel extends HttpKernel {
|
||||||
'App\Http\Middleware\VerifyCsrfToken',
|
'App\Http\Middleware\VerifyCsrfToken',
|
||||||
'App\Http\Middleware\DuplicateSubmissionCheck',
|
'App\Http\Middleware\DuplicateSubmissionCheck',
|
||||||
'App\Http\Middleware\QueryLogging',
|
'App\Http\Middleware\QueryLogging',
|
||||||
'App\Http\Middleware\StartupCheck',
|
|
||||||
'App\Http\Middleware\SessionDataCheckMiddleware',
|
'App\Http\Middleware\SessionDataCheckMiddleware',
|
||||||
|
'App\Http\Middleware\StartupCheck',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,16 @@ class SessionDataCheckMiddleware {
|
||||||
* @param \Closure $next
|
* @param \Closure $next
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function handle($request, Closure $next) {
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
$bag = Session::getMetadataBag();
|
$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 || $elapsed > $max) {
|
||||||
|
$request->session()->flush();
|
||||||
if (($bag && $max < (time() - $bag->getLastUsed()))) {
|
Auth::logout();
|
||||||
|
$request->session()->flash('warning', trans('texts.inactive_logout'));
|
||||||
$request->session()->flush(); // remove all the session data
|
|
||||||
|
|
||||||
Auth::logout(); // logout user
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ if (Utils::isReseller()) {
|
||||||
|
|
||||||
Route::group(['middleware' => 'auth:user'], function() {
|
Route::group(['middleware' => 'auth:user'], function() {
|
||||||
Route::get('dashboard', 'DashboardController@index');
|
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('view_archive/{entity_type}/{visible}', 'AccountController@setTrashVisible');
|
||||||
Route::get('hide_message', 'HomeController@hideMessage');
|
Route::get('hide_message', 'HomeController@hideMessage');
|
||||||
Route::get('force_inline_pdf', 'UserController@forcePDFJS');
|
Route::get('force_inline_pdf', 'UserController@forcePDFJS');
|
||||||
|
|
@ -238,8 +239,8 @@ Route::group([
|
||||||
Route::get('settings/email_preview', 'AccountController@previewEmail');
|
Route::get('settings/email_preview', 'AccountController@previewEmail');
|
||||||
Route::get('company/{section}/{subSection?}', 'AccountController@redirectLegacy');
|
Route::get('company/{section}/{subSection?}', 'AccountController@redirectLegacy');
|
||||||
Route::get('settings/data_visualizations', 'ReportController@d3');
|
Route::get('settings/data_visualizations', 'ReportController@d3');
|
||||||
Route::get('settings/charts_and_reports', 'ReportController@showReports');
|
Route::get('settings/reports', 'ReportController@showReports');
|
||||||
Route::post('settings/charts_and_reports', 'ReportController@showReports');
|
Route::post('settings/reports', 'ReportController@showReports');
|
||||||
|
|
||||||
Route::post('settings/change_plan', 'AccountController@changePlan');
|
Route::post('settings/change_plan', 'AccountController@changePlan');
|
||||||
Route::post('settings/cancel_account', 'AccountController@cancelAccount');
|
Route::post('settings/cancel_account', 'AccountController@cancelAccount');
|
||||||
|
|
@ -411,7 +412,7 @@ if (!defined('CONTACT_EMAIL')) {
|
||||||
define('ACCOUNT_INVOICE_DESIGN', 'invoice_design');
|
define('ACCOUNT_INVOICE_DESIGN', 'invoice_design');
|
||||||
define('ACCOUNT_CLIENT_PORTAL', 'client_portal');
|
define('ACCOUNT_CLIENT_PORTAL', 'client_portal');
|
||||||
define('ACCOUNT_EMAIL_SETTINGS', 'email_settings');
|
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_USER_MANAGEMENT', 'user_management');
|
||||||
define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations');
|
define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations');
|
||||||
define('ACCOUNT_TEMPLATES_AND_REMINDERS', 'templates_and_reminders');
|
define('ACCOUNT_TEMPLATES_AND_REMINDERS', 'templates_and_reminders');
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ class Account extends Eloquent
|
||||||
ACCOUNT_TEMPLATES_AND_REMINDERS,
|
ACCOUNT_TEMPLATES_AND_REMINDERS,
|
||||||
ACCOUNT_BANKS,
|
ACCOUNT_BANKS,
|
||||||
ACCOUNT_CLIENT_PORTAL,
|
ACCOUNT_CLIENT_PORTAL,
|
||||||
ACCOUNT_CHARTS_AND_REPORTS,
|
ACCOUNT_REPORTS,
|
||||||
ACCOUNT_DATA_VISUALIZATIONS,
|
ACCOUNT_DATA_VISUALIZATIONS,
|
||||||
ACCOUNT_API_TOKENS,
|
ACCOUNT_API_TOKENS,
|
||||||
ACCOUNT_USER_MANAGEMENT,
|
ACCOUNT_USER_MANAGEMENT,
|
||||||
|
|
@ -401,11 +401,7 @@ class Account extends Eloquent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function getDate($date = 'now')
|
||||||
* @param string $date
|
|
||||||
* @return DateTime|null|string
|
|
||||||
*/
|
|
||||||
public function getDateTime($date = 'now')
|
|
||||||
{
|
{
|
||||||
if ( ! $date) {
|
if ( ! $date) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -413,6 +409,16 @@ class Account extends Eloquent
|
||||||
$date = new \DateTime($date);
|
$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()));
|
$date->setTimeZone(new \DateTimeZone($this->getTimezone()));
|
||||||
|
|
||||||
return $date;
|
return $date;
|
||||||
|
|
@ -469,7 +475,7 @@ class Account extends Eloquent
|
||||||
*/
|
*/
|
||||||
public function formatDate($date)
|
public function formatDate($date)
|
||||||
{
|
{
|
||||||
$date = $this->getDateTime($date);
|
$date = $this->getDate($date);
|
||||||
|
|
||||||
if ( ! $date) {
|
if ( ! $date) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,15 @@ class Task extends EntityModel
|
||||||
{
|
{
|
||||||
return '#' . $this->public_id;
|
return '#' . $this->public_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDisplayName()
|
||||||
|
{
|
||||||
|
if ($this->description) {
|
||||||
|
return mb_strimwidth($this->description, 0, 16, "...");
|
||||||
|
}
|
||||||
|
|
||||||
|
return '#' . $this->public_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -597,8 +597,11 @@ class BasePaymentDriver
|
||||||
$term = strtolower($matches[2]);
|
$term = strtolower($matches[2]);
|
||||||
$price = $invoice_item->cost;
|
$price = $invoice_item->cost;
|
||||||
if ($plan == PLAN_ENTERPRISE) {
|
if ($plan == PLAN_ENTERPRISE) {
|
||||||
preg_match('/###[\d] [\w]* (\d*)/', $invoice_item->notes, $matches);
|
if (count($matches)) {
|
||||||
$numUsers = $matches[1];
|
$numUsers = $matches[1];
|
||||||
|
} else {
|
||||||
|
$numUsers = 5;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$numUsers = 1;
|
$numUsers = 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,148 @@
|
||||||
<?php namespace App\Ninja\Repositories;
|
<?php namespace App\Ninja\Repositories;
|
||||||
|
|
||||||
|
use stdClass;
|
||||||
use DB;
|
use DB;
|
||||||
use App\Models\Activity;
|
use App\Models\Activity;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\Task;
|
||||||
|
use DateInterval;
|
||||||
|
use DatePeriod;
|
||||||
|
|
||||||
class DashboardRepository
|
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)
|
public function totals($accountId, $userId, $viewAll)
|
||||||
{
|
{
|
||||||
// total_income, billed_clients, invoice_sent and active_clients
|
// total_income, billed_clients, invoice_sent and active_clients
|
||||||
|
|
@ -193,4 +331,33 @@ class DashboardRepository
|
||||||
->take(50)
|
->take(50)
|
||||||
->get();
|
->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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@
|
||||||
"stacktrace-js": "~1.0.1",
|
"stacktrace-js": "~1.0.1",
|
||||||
"fuse.js": "~2.0.2",
|
"fuse.js": "~2.0.2",
|
||||||
"dropzone": "~4.3.0",
|
"dropzone": "~4.3.0",
|
||||||
"sweetalert": "~1.1.3"
|
"sweetalert": "~1.1.3",
|
||||||
|
"bootstrap-daterangepicker": "~2.1.24"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"jquery": "~1.11"
|
"jquery": "~1.11"
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'lifetime' => env('SESSION_LIFETIME', 120),
|
'lifetime' => env('SESSION_LIFETIME', (60 * 8)),
|
||||||
|
|
||||||
'expire_on_close' => false,
|
'expire_on_close' => false,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ elixir(function(mix) {
|
||||||
'fonts.css'
|
'fonts.css'
|
||||||
], 'public/css/built.css');
|
], 'public/css/built.css');
|
||||||
|
|
||||||
|
mix.styles([
|
||||||
|
bowerDir + '/bootstrap-daterangepicker/daterangepicker.css'
|
||||||
|
], 'public/css/daterangepicker.css');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JS configuration
|
* JS configuration
|
||||||
*/
|
*/
|
||||||
|
|
@ -71,6 +76,10 @@ elixir(function(mix) {
|
||||||
'vfs.js'
|
'vfs.js'
|
||||||
], 'public/pdf.built.js');
|
], 'public/pdf.built.js');
|
||||||
|
|
||||||
|
mix.scripts([
|
||||||
|
bowerDir + '/bootstrap-daterangepicker/daterangepicker.js'
|
||||||
|
], 'public/js/daterangepicker.min.js');
|
||||||
|
|
||||||
mix.scripts([
|
mix.scripts([
|
||||||
bowerDir + '/jquery/dist/jquery.js',
|
bowerDir + '/jquery/dist/jquery.js',
|
||||||
bowerDir + '/jquery-ui/jquery-ui.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
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -100,8 +100,8 @@
|
||||||
|
|
||||||
#right-sidebar-wrapper .sidebar-nav li {
|
#right-sidebar-wrapper .sidebar-nav li {
|
||||||
text-indent: 8px;
|
text-indent: 8px;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
line-height: 44px;
|
line-height: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#right-sidebar-wrapper .sidebar-nav li a.btn {
|
#right-sidebar-wrapper .sidebar-nav li a.btn {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2099,6 +2099,15 @@ $LANG = array(
|
||||||
'facebook_and_twitter_help' => 'Follow our feeds to help support our project',
|
'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.',
|
'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',
|
'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;
|
return $LANG;
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,214 @@
|
||||||
@extends('header')
|
@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')
|
@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>
|
||||||
|
<span></span> <b class="caret"></b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<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 style="overflow:hidden">
|
||||||
<div class="in-thin">
|
<div class="in-thin">
|
||||||
{{ trans('texts.total_revenue') }}
|
{{ trans('texts.total_revenue') }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="revenue-div in-bold pull-right" style="color:#337ab7">
|
||||||
|
</div>
|
||||||
<div class="in-bold">
|
<div class="in-bold">
|
||||||
@if (count($paidToDate))
|
@if (count($paidToDate))
|
||||||
@foreach ($paidToDate as $item)
|
@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
|
@endforeach
|
||||||
@else
|
@else
|
||||||
{{ Utils::formatMoney(0) }}
|
{{ Utils::formatMoney(0) }}
|
||||||
|
|
@ -29,21 +221,38 @@
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<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 style="overflow:hidden">
|
||||||
<div class="in-thin">
|
@if (count($expenses))
|
||||||
{{ trans('texts.average_invoice') }}
|
<div class="in-thin">
|
||||||
</div>
|
{{ trans('texts.total_expenses') }}
|
||||||
<div class="in-bold">
|
</div>
|
||||||
@if (count($averageInvoice))
|
<div class="expenses-div in-bold pull-right" style="color:#337ab7">
|
||||||
@foreach ($averageInvoice as $item)
|
</div>
|
||||||
{{ Utils::formatMoney($item->invoice_avg, $item->currency_id) }}<br/>
|
<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
|
@endforeach
|
||||||
@else
|
</div>
|
||||||
{{ Utils::formatMoney(0) }}
|
@else
|
||||||
@endif
|
<div class="in-thin">
|
||||||
</div>
|
{{ 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -51,16 +260,18 @@
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<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 style="overflow:hidden">
|
||||||
<div class="in-thin">
|
<div class="in-thin">
|
||||||
{{ trans('texts.outstanding') }}
|
{{ trans('texts.outstanding') }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="outstanding-div in-bold pull-right" style="color:#337ab7">
|
||||||
|
</div>
|
||||||
<div class="in-bold">
|
<div class="in-bold">
|
||||||
@if (count($balances))
|
@if (count($balances))
|
||||||
@foreach ($balances as $item)
|
@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
|
@endforeach
|
||||||
@else
|
@else
|
||||||
{{ Utils::formatMoney(0) }}
|
{{ Utils::formatMoney(0) }}
|
||||||
|
|
@ -72,13 +283,23 @@
|
||||||
</div>
|
</div>
|
||||||
</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> </p>
|
<p> </p>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default dashboard" style="height:320px">
|
<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">
|
<h3 class="panel-title in-bold-white">
|
||||||
<i class="glyphicon glyphicon-exclamation-sign"></i> {{ trans('texts.activity') }}
|
<i class="glyphicon glyphicon-exclamation-sign"></i> {{ trans('texts.activity') }}
|
||||||
@if ($invoicesSent)
|
@if ($invoicesSent)
|
||||||
|
|
@ -103,6 +324,17 @@
|
||||||
<div class="panel panel-default dashboard" style="height:320px;">
|
<div class="panel panel-default dashboard" style="height:320px;">
|
||||||
<div class="panel-heading" style="margin:0; background-color: #f5f5f5 !important;">
|
<div class="panel-heading" style="margin:0; background-color: #f5f5f5 !important;">
|
||||||
<h3 class="panel-title" style="color: black !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') }}
|
<i class="glyphicon glyphicon-ok-sign"></i> {{ trans('texts.recent_payments') }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -172,7 +404,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default dashboard" style="height:320px">
|
<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">
|
<h3 class="panel-title in-bold-white">
|
||||||
<i class="glyphicon glyphicon-time"></i> {{ trans('texts.invoices_past_due') }}
|
<i class="glyphicon glyphicon-time"></i> {{ trans('texts.invoices_past_due') }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -242,7 +474,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default dashboard" style="height:320px">
|
<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">
|
<h3 class="panel-title in-bold-white">
|
||||||
<i class="glyphicon glyphicon-time"></i> {{ trans('texts.expired_quotes') }}
|
<i class="glyphicon glyphicon-time"></i> {{ trans('texts.expired_quotes') }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
<link href="{{ asset('css/built.css') }}?no_cache={{ NINJA_VERSION }}" rel="stylesheet" type="text/css"/>
|
<link href="{{ asset('css/built.css') }}?no_cache={{ NINJA_VERSION }}" rel="stylesheet" type="text/css"/>
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
||||||
@if (Auth::check() && Auth::user()->dark_mode)
|
@if (Auth::check() && Auth::user()->dark_mode)
|
||||||
body {
|
body {
|
||||||
background: #000 !important;
|
background: #000 !important;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
for (var i=0; i<currencies.length; i++) {
|
for (var i=0; i<currencies.length; i++) {
|
||||||
var currency = currencies[i];
|
var currency = currencies[i];
|
||||||
currencyMap[currency.id] = currency;
|
currencyMap[currency.id] = currency;
|
||||||
|
currencyMap[currency.code] = currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
var countries = {!! \Cache::get('countries') !!};
|
var countries = {!! \Cache::get('countries') !!};
|
||||||
|
|
@ -28,7 +29,7 @@
|
||||||
NINJA.parseFloat = function(str) {
|
NINJA.parseFloat = function(str) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
str = (str+'').replace(/[^0-9\.\-]/g, '');
|
str = (str+'').replace(/[^0-9\.\-]/g, '');
|
||||||
|
|
||||||
return window.parseFloat(str);
|
return window.parseFloat(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,4 +98,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,14 @@
|
||||||
} else {
|
} else {
|
||||||
// response contains id and card, which contains additional card details
|
// response contains id and card, which contains additional card details
|
||||||
var token = response.id;
|
var token = response.id;
|
||||||
// Insert the token into the form so it gets submitted to the server
|
if (token) {
|
||||||
$form.append($('<input type="hidden" name="sourceToken"/>').val(token));
|
// Insert the token into the form so it gets submitted to the server
|
||||||
// and submit
|
$form.append($('<input type="hidden" name="sourceToken"/>').val(token));
|
||||||
$form.get(0).submit();
|
// and submit
|
||||||
|
$form.get(0).submit();
|
||||||
|
} else {
|
||||||
|
logError(JSON.stringify(response));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
@extends('header')
|
@extends('header')
|
||||||
|
|
||||||
@section('head')
|
|
||||||
@parent
|
|
||||||
|
|
||||||
<script src="{!! asset('js/Chart.min.js') !!}" type="text/javascript"></script>
|
|
||||||
@stop
|
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
@parent
|
@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') !!}
|
{!! Former::open()->rules(['start_date' => 'required', 'end_date' => 'required'])->addClass('warn-on-exit') !!}
|
||||||
|
|
@ -54,18 +48,12 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<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')) !!}
|
{!! Former::select('report_type')->options($reportTypes, $reportType)->label(trans('texts.type')) !!}
|
||||||
<div id="dateField" style="display:{{ $reportType == ENTITY_TAX_RATE ? 'block' : 'none' }}">
|
<div id="dateField" style="display:{{ $reportType == ENTITY_TAX_RATE ? 'block' : 'none' }}">
|
||||||
{!! Former::select('date_field')->label(trans('texts.filter'))
|
{!! Former::select('date_field')->label(trans('texts.filter'))
|
||||||
->addOption(trans('texts.invoice_date'), FILTER_INVOICE_DATE)
|
->addOption(trans('texts.invoice_date'), FILTER_INVOICE_DATE)
|
||||||
->addOption(trans('texts.payment_date'), FILTER_PAYMENT_DATE) !!}
|
->addOption(trans('texts.payment_date'), FILTER_PAYMENT_DATE) !!}
|
||||||
</div>
|
</div>
|
||||||
<p> </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() !!}
|
{!! Former::close() !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -73,7 +61,6 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if ($enableReport)
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<table class="table table-striped invoice-table">
|
<table class="table table-striped invoice-table">
|
||||||
|
|
@ -128,29 +115,6 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
|
|
||||||
@if ($enableChart)
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-body">
|
|
||||||
<canvas id="monthly-reports" width="700" height="400"></canvas>
|
|
||||||
<p> </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"> 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"> 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"> Credits</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -162,32 +126,6 @@
|
||||||
$('#action').val('');
|
$('#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() {
|
$(function() {
|
||||||
$('.start_date .input-group-addon').click(function() {
|
$('.start_date .input-group-addon').click(function() {
|
||||||
toggleDatePicker('start_date');
|
toggleDatePicker('start_date');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue